JavaScript深入 — 原型与原型链
- 一、原因
- 二、使用class实现继承
- 普通的类
- 实现继承
- 三、原型
- 四、原型链
- 小结
- 原型
- 原型链
- prototype和proto
- 引申
一、原因
JavaScript中除了基础类型外的数据类型,都是对象(引用类型)。但是由于其没有类(class
)的概念,如何将所有对象联系起来就成了一个问题,于是就有了原型和原型链的概念。
原型链是JavaScript中实现对象属性继承的一种机制。
原型:构造函数会有一个原型对象 prototype
,保存了该构造函数,以及定义的方法;而由构造函数生成的实例,则有一个私有属性 [[prototype]]
,也同样指向该构造函数的 prototype
(在很多环境下都可以使用 __proto__
访问,但现在已经提供了接口,我们可以通过 Object.getPrototypeOf(instance)
访问。
原型链:前面我们提到的构造函数的 prototype
,也有私有属性[[prototype]]
指向另一个 prototype
,最终指向 Object.prototype
,该原型的__proto__
指向 null
。
class Teacher {
constructor(name){
this.name = name;
}
sayHi(){
console.log(this.name + ": hi")
}
}
const teacherA = new Teacher("A");
👆 如上代码片段解析
二、使用class实现继承
因为JS中没有类(class
)这个概念,所以JS的设计者使用了构造函数来实现继承机制
ES6中的
class
可以看作只是一个语法糖,它的绝大部分的功能,ES5都可以做到,新的class
写法只是让原型的写法更加的清晰、更像面向对象编程的语法而已。下文也会进一步的说明。 —— 摘自阮一峰的ES6入门
普通的类
class Student {
constructor(name, score) {
this.name = name;
this.score = score;
}
introduce() {
console.log(`I am ${this.name}, I got ${this.score} in the exam.`);
}
}
const student = new Student("小明", 59);
console.log("student", student);
student.introduce();
实现继承
class Person {
constructor(name) {
this.name = name;
}
}
class Student extends Person {
constructor(name, score) {
super(name);
this.score = score;
}
}
三、原型
🌰 依然是上面的例子,当我们在控制台打印 student
时,我们发现其中并没有 introduce
这个方法
展开 [[Prototype]]
(或__proto__
)后,我们可以看到 introduce
方法,而这个[[Prototype]]
就是 student
这个对象的隐式原型
而 Student
类上会有一个属性 prototype
,通过验证可知其与student.__proto__
指向同一地址空间,而 Student.prototype
被称为显式原型
当我们在一个对象上尝试查找一个属性或方法时,如果找不到对应的属性或方法,就会往它的隐式原型上去找
四、原型链
🌰 依然是上面的例子
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`Hi, I am ${this.name}`);
}
}
class Student extends Person{
constructor(name, score) {
super(name);
this.score = score;
}
introduce() {
console.log(`I am ${this.name}, I got ${this.score} in the exam.`);
}
}
👇 打印 student
有如下结果
👇 完整原型链如下
小结
原型
在JavaScript中,每当定义一个对象(函数也是对象)时候,对象中都会包含一些预定义的属性。其中每个函数对象都有一个 prototype
属性,这个属性指向函数的原型对象,使用原型对象的好处是所有对象实例共享它所包含的属性和方法
原型链
每个实例对象( object
)都有一个私有属性(称之为 proto
)指向它的构造函数的原型对象(prototype
)。该原型对象也有一个自己的原型对象( proto
) ,层层向上直到一个对象的原型对象为 null
。根据定义,null
没有原型,并作为这个原型链中的最后一个环节
prototype和proto
prototype
是构造函数的属性__proto__
是每个实例都有的属性,可以访问[[prototype]]
属性,当然[[prototype]]
也有__proto__
属性- 实例的
__proto__
与其构造函数的prototype
指向的是同一个对象
__proto__
并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用Object.getPrototypeOf
方法来获取实例对象的原型,然后再来为原型添加方法/属性。
——摘自阮一峰的ES6入门
引申
为什么JS不像C++/Java那样用类、指针这种,而是用原型和原型链?
🌰 看代码说输出:
function Person(name) {
this.name = name;
}
const person = new Person("Tom");
console.log(Person.prototype); // {}
console.log(Person.__proto__); // ƒ () { [native code] }
console.log(person.prototype); // undefined
console.log(person.__proto__); // {}
Object
和 Object.prototype
:
复习时突然有点混淆上述两者了,以为 Object
是原型链终点,其实是 Object
的原型,即 Object.prototype
。
Object
本身是一个构造函数,所以Object.__proto__
其实指向的是Function.prototype
,而Function.prototype.__proto__
指向了 Object.prototype
。
- 构造函数既有
prototype
,指向自己的原型;又有__proto__
,指向其继承的构造函数。 - 构造函数继承原型链的顶端Function,其
__proto___
指向自己的prototype
,如上图,使得Object
和Function
在原型链顶端形成了一个类似“环形”的链,第一次遇到时可能难以想明白。
这也解释了我的疑惑,为什么判断类型的方法不能用 Object.toString
,而是Object.prototype.toString
,二者同名但不是一个方法。
前者沿原型链找到 Function.prototype
,调用其 toString
方法,而后者则是Object
原型上定义的方法。
console.log(Object.toString === Object.prototype.toString); // false
console.log(Object.toString === Function.prototype.toString); // true