原型链的概念
由于在JS世界中,函数其实也是个对象,所以函数可以拥有属性,JS规定了所有的函数都默认拥有一个叫做prototype
的属性,这个属性指向了另一个对象。比如我们声明一个function:
function Person() {}
console.log(Person.prototype);
打印出来的结果为:
可以看到,函数的prototype默认拥有两个属性:constructor
和__proto__
;我们可以给函数的prototype对象添加更多的自定义属性,比如,我想给Person函数添加一个getName
方法,可以这么做:
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
}
console.log(Person.prototype);
现在prototype里面已经有个自定义的方法了:
我们可以通过
new
关键字实例化一个对象,比如上面代码可以这样写:const p = new Person('张三')
,此时,Person函数被称为构造函数,可以理解为是用来构造对象的函数,p被称为实例对象,现在函数内部的this
指向的就是这个实例对象了,p.name
得到的结果便是“张三”。
可以通过instanceof
判断一个对象是否是某个构造函数的实例:
p instanceof Person; // true
注意,构造函数首字母需要大写,所以截图中的构造函数声明是不规范的。
令人困惑的是,我们可以通过这个实例对象去调用绑定在Person.prototype
上的方法,比如上述例子中通过p.getName()
也可以拿到结果“张三”,看起来似乎是创建p
的时候会把Person.prototype
对象复制到这个实例对象中。然而事实并不是这样。
我们可以console.log(p)
查看下p中的内容,得到结果:
可以看到:p
实例对象中有一个默认的__proto__
对象,而getName
方法就在__proto__
对象中。在JavaScript中,每个实例对象都有一个私有属性__proto__
,指向它的构造函数的原型对象prototype
。
p.__proto__ === Person.prototype; // true
事实上,所有的对象都具有__proto__
属性,因为所有的对象都是Object
的实例对象:
var o = {};
o instanceof Object; // true
o.__proto__ === Object.prototype; // true
重点: 当试图访问一个对象的属性时,它会先在该对象本身内查找,如果没有查到,则会在该对象的原型上查找,如果还没找到,则继续在该对象的原型的原型上查找,层层向上直到一个原型对象为null。这条“链”我们称之为原型链。
好了,根据上面的描述,我们来看下这段代码的原型查找是怎么样的:
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
}
const p = new Person('张三');
p.toString(); // [object Object]
- p查找本身有没有toString方法
- p本身没有该方法,则查找其构造函数原型上有无此方法
-
p.__proto__
上也没有,则继续查找p.__proto__.__proto__
对象上有无此方法,找到并调用:
由上所知,下面这个等式也是成立的:
Person.prototype.__proto__ === Object.prototype; // true
ok,还有个问题,Person也是个对象,那Person.__proto__
指向的是谁呢?答案是Function.prototype
,所有的函数都是Function
的实例,而Function.__proto__
指向Function.prototype
:
Person instanceof Function; // true
Person.__proto__ === Function.prototype; // true
Function.__proto__ === Function.prototype; // true
上述便是一个构造函数的整个原型链的结构,简单总结一下:
- 所有的JS对象都存在
__proto__
属性,只有函数对象才有prototype
属性,所以函数对象既有__proto__
也有prototype
- 构造函数的实例对象的
__proto__
指向构造函数的prototype
,普通对象都是Object的实例,在没有特别处理的情况下,普通对象的__proto__
指向Object.prototype
继承
这里我们只讨论基于prototype的继承,假设我们有两个构造函数:
// 定义一个父类
function Parent() {}
Parent.prototype.getName = function() {
return 'parent\'s name';
}
// 定义一个子类
function Child() {}
我们可以直接通过prototype赋值的写法进行继承:
Child.prototype = Parent.prototype;
这样,所有挂载在Parent.prototype
上的方法和属性都能被继承下来了:
const child = new Child();
console.log(child.getName()); // parent's name
但这种做法有两个缺点,明显的是,直接用prototype
赋值之后,Child.prototype
和Parent.prototype
现在指向了同一个对象,任何对Child.prototype
的更改都会直接影响Parent.prototype
;还有个不明显的缺点,上文中说到,函数的prototype有个默认的属性叫做constructor
,指向的是构造器本身:
Child.prototype.constructor === Child; // true
Parent.prototype.constructor === Parent; // true
赋值之后,Child.prototype.constructor
的指向变成了Parent
!一般的做法是手动将指向改回来:
Child.prototype.constructor = Child;
但我们并没有解决两个prototype绑定在了一起的问题。我们可以用另一种基于prototype的方法实现继承,即使用Object.create()
方法:
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true
}
});
关于Object.create()
的详细解释,请看MDN。上述的意思大致是创造一个基于Parent.prototype的对象并赋值给Child.prototype,其中第二个参数的意思是设置这个新创造对象的constructor
属性,里面的设置内容对应Object.defineProperty的第三个参数(即属性描述符),可以看到这里设置了constructor
属性的value为Child
,即将Child.prototype.constructor
指向Child
;
上述代码还需要完善下,因为我们虽然实现了Child继承了Parent,且解决了prototype的指向问题,但是Child.__proto__
现在仍然指向的是Function.prototype
,为了继承的完整性,我们需要将Child.__proto__
指向Parent
:
Object.setPrototypeOf
? Object.setPrototypeOf(Child, Parent)
: Child.__proto__ = Parent;
这个基于Object.create()
的继承方式,就是ES6的class
中extends
的实现原理。
在普通对象中使用Object.create()
,比如:
const origin = {
name: 'origin'
};
const o = Object.create(origin);
此时o.__proto__
指向的不再是Object.prototype
了,而是origin,并且o已经继承了origin对象的所有属性(无论是否可枚举):
o.__proto__ === origin; // true
console.log(o.name); // 'origin'
我们可以使用Object.getPrototypeOf()获取当前对象的原型:
console.log(Object.getPrototypeOf(o)); // {name: "origin'}
可以使用Object.getOwnPropertyNames()获取一个对象的自身属性(即不是继承下来的属性),返回的是自身所有属性(无论是否可枚举)的数组:
console.log(Object.getOwnPropertyNames(o)); // []
console.log(Object.getOwnPropertyNames(origin)); // ["name"]
可以看到, Object.getOwnPropertyNames()
与Object.keys()
相比,后者仅返回自身的可枚举属性,而不可枚举不会被返回。
到这里,差不多已经把JS中原型的概念都介绍了,看完这篇文章并且能真的理解的话,JS原型的知识肯定不成问题了。