本文以 zh.javascript.info/prototype-i… 为基础,并加入一点自己的理解。
原型
在 JavaScript 中,对象有一个特殊的隐藏属性 [[Prototype]]
,它要么为 null
,要么就是对另一个对象的引用,该对象被称为“原型”。
let animal = {
eats: true
};
console.log(animal);
打印 animal 对象,可以看到隐藏的 [[Prototype]]
对象。
打开[[Prototype]]
可以看到 animal 对象继承了 Object.prototype
的各种属性。
(到现在,你可能不理解什么是Object.prototype
,没关系,先继续往下面看)
原型继承
当我们从对象中读取一个缺失的属性时,JavaScript 会自动从原型中获取该属性。 在编程中,这种行为被称为“原型继承”。
animal 对象并没有toString
方法。
但它从其原型(即Object.prototype
)中继承了该方法。
设置原型
属性 [[Prototype]]
是内部的而且是隐藏的,但是这儿有很多设置它的方式。
法一:
使用特殊的名字 __proto__
,在现代JavaScript中,__proto__
是可读可写的。
let animal = { eats: true };
let rabbit = { jumps: true };
rabbit.__proto__ = animal; // 设置 rabbit.[[Prototype]] = animal
现在,如果我们从 rabbit
中读取一个它没有的属性,JavaScript 会自动从 animal
中获取。
console.log(rabbit.eates); //true
原型可以很长,层层继承就形成了原型链。
let animal = {
eats: true,
walk(){
console.log('Animal walk');
}
};
let rabbit = {
__proto__:animal,
jumps:true
}
let longEar = {
earLength:10,
__proto__:rabbit
}
// walk()和jumps都是继承而来的
longEar.walk(); //Animal Walk
console.log(longEar.jumps); //true
可能有人要脑洞大开了,让
animal
再继承LongEar
形成闭环会发生什么?
let animal = {
eats: true,
walk(){
console.log('Animal walk');
}
};
let rabbit = {
__proto__:animal,
jumps:true
}
let longEar = {
earLength:10,
__proto__:rabbit
}
animal.__proto__ = longEar;
会报 TypeError
。
最后,有4个点要注意:
- 原型链不能形成闭环。
__proto__
的值只能是对象或者null
,其他的类型都会被忽略。- 一个对象不能从其他两个对象获得继承。一个对象只能有一个
[[Prototype]]
。 _proto__
与内部的[[Prototype]]
不一样。__proto__
是[[Prototype]]
的 getter/setter。
法二:
尽管连node也支持__proto__
这种写法,但是我们应该使用Object.getPrototypeOf()
和Object.setPrototypeOf()
来替代。
let animal = {eats: true};
console.log(Object.getPrototypeOf(animal) === Object.prototype); //true
let animal = {eats: true};
let rabbit = {jumps:true};
Object.setPrototypeOf(rabbit,animal); //将animal设置为rabbit的原型
console.log(rabbit);
法三:
使用Object.create(proto)
,用给定的 proto
作为原型创建一个空对象。
let animal = {eats: true};
let rabbit = Object.create(animal); //让animal作为rabbit的原型
我们使用对象字面量或者Object构造函数创建的对象会默认继承自Object.prototype
。
let obj1 = {}; //对象字面量
let obj2 = new Object(); //构造函数
如果我们使用Object.create()
创建对象时,且Object.prototype
为原型,最终的结果会和上面一样。
let obj3 = Object.create(Object.prototype);
console.log(obj3);
不过,如果让它以 null
为原型,就会创建一个真正空空如也的对象。
let obj4 = Object.create(null);
console.log(obj4);
函数.prototype
前面一直在说Object.prototype
,那么这个到底是个什么玩意儿?
我们知道,可以通过new 构造函数名()
来创建一个新的对象。在这里,将构造函数名简写为F
,也即 new F()
。
其实不仅构造函数,任何函数都有 prototype
属性,即使我们没有提供它。
如果 F.prototype
是一个对象,那么 new
操作符会使用它为新对象设置 [[Prototype]]
。
这里的 F.prototype
指的是 F
的一个名为 "prototype"
的常规属性。这听起来与“原型”这个术语很类似,但这里我们实际上指的是具有该名字的常规属性。
let animal = {eats: true};
function Rabbit(name){
this.name = name;
}
// 将Rabbit函数的prototype属性值设置为animal对象
Rabbit.prototype = animal;
let rabbit = new Rabbit("White Rabbit");
// rabbit.__proto__ == animal
console.log(rabbit.eats); //true
设置 Rabbit.prototype = animal
的字面意思是:“当创建了一个 new Rabbit
时,把它的 [[Prototype]]
赋值为 animal
”。
F.prototype
属性仅在 new F
被调用时使用,它为新对象的 [[Prototype]]
赋值。
如果在创建之后,F.prototype
属性有了变化(F.prototype = <another object>
),那么通过 new F
创建的新对象也将随之拥有新的对象作为 [[Prototype]]
,但已经存在的对象将保持旧有的值。
函数默认的prototype
每个函数都有prototype
属性,默认的 "prototype"
是一个只有属性 constructor
的对象,属性 constructor
指向函数自身。
我们可以检查一下:
fuction Rabbit(){}
console.log(Rabbit.prototype.constuctor === Rabbit) // true
通常,如果我们什么都不做,constructor
属性可以通过 [[Prototype]]
给所有 rabbits 使用:
function Rabbit() {}
// by default:
// Rabbit.prototype = { constructor: Rabbit }
let rabbit = new Rabbit(); // 继承自{constructor: Rabbit}
console.log(rabbit.constructor == Rabbit); // true (from prototype)
我们可以使用 constructor
属性来创建一个新对象,该对象使用与现有对象相同的构造器。因为新创建的对象继承的constuctor指向原来的构造函数!
function Rabbit(name) {
this.name = name;
alert(name);
}
let rabbit = new Rabbit("White Rabbit");
let rabbit2 = new rabbit.constructor("Black Rabbit");
但想得到好,……JavaScript 自身并不能确保正确的 "constructor"
函数值。
比如我们如果将函数的prototype属性直接替换掉,那就不会存在constructor
属性。
function Rabbit(){}
Rabbit.prototype = {eats: true};
let rabbit = new Rabbit();
console.log(rabbit.constructor === Rabbit); //false
因此,为了确保正确的 "constructor"
,我们可以选择添加/删除属性到默认 "prototype"
,而不是将其整个覆盖:
Rabbit.prototype.jumps = true;
// 默认的 Rabbit.prototype.constructor 被保留了下来
或者,也可以手动重新创建 constructor
属性:
Rabbit.prototype = { jumps: true, constructor: Rabbit};
Object.prototype
let obj = new Object();
//其实和 let obj = {};是一样的
还记得上面的说的:如果 F.prototype
是一个对象,那么 new
操作符会使用它为新对象设置 [[Prototype]]
。
Object
就是一个内建的对象构造函数,其自身的 prototype
指向一个带有 toString
和其他方法的一个巨大的对象。上面的创建对象的操作就相当于:
// 创建新对象
let obj = new Object();
//默认进行了
obj.__proto__ = Object.prototype;
console.log(obj.__proto__ === Object.prototype); //true
另外 Object.prototype
上方的链中没有更多的 [[Prototype]]
。
另外,咱们也不能给 Object.prototype
继续设置原型对象了。
其他内建对象继承自Object.prototype
其他内建对象,像 Array
、Date
、Function
及其他,都在 prototype 上挂载了方法。
例如,当我们创建一个数组 [1, 2, 3]
,在内部会默认使用 new Array()
构造器。因此 Array.prototype
变成了这个数组的 prototype,并为这个数组提供数组的操作方法。这样内存的存储效率是很高的。
按照规范,所有的内建原型顶端都是 Object.prototype
。这就是为什么有人说“一切都从对象继承而来”。
让我们来验证一下:
let arr = [1, 2, 3];
arr.__proto__ === Array.prototype; // true
//或者说
Array.prototype.isPrototypeOf(arr); //true
Object.prototype.isPrototypeOf(arr); //true
总结
前言万语,不如手绘一张图。
构造函数与prototype
与[[Prototype]]
的关系:
如果说,继续从 那个普通的构造函数出发,一直画原型链:
另外,我们也可以从 F.prototype
出发,继续往上画。
而这只是最简单的情况,就已经非常之复杂了。
//验证代码
function F() {}
const obj = new F();
// 验证 obj的直接原型是否为F.prototype
console.log(Object.getPrototypeOf(obj) === F.prototype); //true
// 验证F.prototype的原型是否为 Object.prototype
console.log(Object.getPrototypeOf(F.prototype) === Object.prototype); //true
// 验证普通构造函数F的直接原型是否为Function.prototype
console.log(Object.getPrototypeOf(F) === Function.prototype); //true
// 验证Function.prototype 的原型是否为 Object.prototype
console.log(Object.getPrototypeOf(Function.prototype) === Object.prototype); //true
就这样简单的function F() {} const obj = new F();
就已经产生超级复杂的原型链了,而这只是最简单的情况。