在编程中,我们经常会想获取并扩展一些东西。
例如,我们有一个 user 对象及其属性和方法,并希望将 admin 和 guest 作为基于 user 稍加修改的变体。我们想重用 user 中的内容,而不是复制/重新实现它的方法,而只是在其之上构建一个新的对象。
原型继承(Prototypal inheritance) 这个语言特性能够帮助我们实现这一需求。
在 JavaScript 中,对象有一个特殊的隐藏属性 [Prototype](Prototype.md)(原型)(如规范中所命名的),它要么为 null,要么就是对另一个对象的引用。该对象被称为“原型”。
当我们从 object 中读取一个缺失的属性时,JavaScript 会自动从原型中获取该属性。在编程中,这被称为“原型继承”。
属性 [Prototype](Prototype.md) 是内部的而且是隐藏的,但是这儿有很多设置它的方式。
使用特殊的名字 __proto__:
let animal = {
eats: true,
}
let rabbit = {
jumps: true,
}
rabbit.__proto__ = animal;
console.log(rabbit.eats);//true
rabbit.eat();//eating!!!如果 animal 有许多有用的属性和方法,那么它们将自动地变为在 rabbit 中可用。这种属性被称为“继承”。
原型链可以很长,但是有两个限制:
- 不能形成闭环
——proto——的值只能是对象或者null,其他的值将会自动的被忽略
只能有一个 [Prototype](Prototype.md)。一个对象不能从其他两个对象获得继承。
__proto__ 是 [Prototype](Prototype.md) 的因历史原因而留下来的 getter/setter 初学者常犯一个普遍的错误,就是不知道 __proto__ 和 [Prototype](Prototype.md) 的区别。 请注意,__proto__ 与内部的 [Prototype](Prototype.md) 不一样。__proto__ 是 [Prototype](Prototype.md) 的 getter/setter。
__proto__ 属性有点过时了。它的存在是出于历史的原因,现代编程语言建议我们应该使用函数 Object.getPrototypeOf/Object.setPrototypeOf 来取代 __proto__ 去 get/set 原型。
在写入和删除的时候,使用的是直接的对象,而不是原型。读取时是原型:
let animal = {
eats: true,
walk() {
console.log('animal walk!!!');
}
};
let rabbit = {
__proto__: animal
};
rabbit.walk();//animal walk!!!
rabbit.walk = function () {
console.log("Rabbit! Bounce-bounce!");
};
rabbit.walk(); // Rabbit! Bounce-bounce!
animal.walk();//animal walk!!!
访问器(accessor)属性是一个例外,因为赋值(assignment)操作是由 setter 函数处理的。因此,写入此类属性实际上与调用函数相同。
let user = {
name: "John",
surname: "Smith",
set fullName(value) {
[this.name, this.surname] = value.split(" ");
},
get fullName() {
return `${this.name} ${this.surname}`;
}
};
let admin = {
__proto__: user,
isAdmin: true
};
console.log(admin.fullName); // John Smith (*)
admin.name = 'Dano';
console.log(admin.fullName);//Dano Smith
console.log(user.fullName);//John Smith
console.log(user.name);//John
admin.fullName = "Alice Cooper"; //(**)
console.log(admin.fullName); // Alice Cooper,admin 的内容被修改了
console.log(user.fullName); // John Smith,user 的内容被保护了在 (*) 行中,属性 admin.fullName 在原型 user 中有一个 getter,因此它会被调用。在 (**) 行中,属性在原型中有一个 setter,因此它会被调用。
在上面的例子中可能会出现一个有趣的问题:在 set fullName(value) 中 this 的值是什么?属性 this.name 和 this.surname 被写在哪里:在 user 还是 admin?
答案很简单:this 根本不受原型的影响。 无论在哪里找到方法:在一个对象还是在原型中。在一个方法调用中,this 始终是点符号 . 前面的对象。 因此,setter 调用 admin.fullName= 使用 admin 作为 this,而不是 user。
也就是说,普通的属性写入是在对象上进行的。对于访问器属性(虚假的属性),他是调用的原型的方法来实现的,方法中的this却是对象的而不是原型的,那么设置的也就是对象的属性。
所以,方法是共享的,但对象状态不是。
Object.keys(obj)只会返回自己的属性,而不会返回原型的属性 而for...in会返回原型和对象的属性。使用内建方法obj.hasOwnProperty(key)如果属性是自己的就返回true,是原型的就返回false。
let user = {
name: "John",
surname: "Smith",
set fullName(value) {
[this.name, this.surname] = value.split(" ");
},
get fullName() {
return `${this.name} ${this.surname}`;
}
};
let admin = {
__proto__: user,
isAdmin: true
};
console.log(Object.keys(admin));//[ 'isAdmin' ]
for (let prop in admin) console.log(prop);
/**
isAdmin
name
surname
fullName
*/
for (let prop in admin) {
if (admin.hasOwnProperty(prop)) {
console.log('对象的属性:' + prop);
} else {
console.log('原型的属性:' + prop);
}
};
/**
* 对象的属性:isAdmin
原型的属性:name
原型的属性:surname
原型的属性:fullName
*/这里我们有以下继承链:rabbit 从 animal 中继承,animal 从 Object.prototype 中继承(因为 animal 是对象字面量 {...},所以这是默认的继承),然后再向上是 null
这有一件很有趣的事儿。方法 rabbit.hasOwnProperty 来自哪儿?我们并没有定义它。从上图中的原型链我们可以看到,该方法是 Object.prototype.hasOwnProperty 提供的。换句话说,它是继承的。
……如果 for..in 循环会列出继承的属性,那为什么 hasOwnProperty 没有像 eats 和 jumps 那样出现在 for..in 循环中?
答案很简单:它是不可枚举的。就像 Object.prototype 的其他属性,hasOwnProperty 有 enumerable:false 标志。并且 for..in 只会列出可枚举的属性。这就是为什么它和其余的 Object.prototype 属性都未被列出。
在现代引擎中,从性能的角度来看,我们是从对象还是从原型链获取属性都是没区别的。它们(引擎)会记住在哪里找到的该属性,并在下一次请求中重用它。
构造函数的Prototype
这里的 F.prototype 指的是 F 的一个名为 "prototype" 的常规属性。这听起来与“原型”这个术语很类似,但这里我们实际上指的是具有该名字的常规属性。
设置 Rabbit.prototype = animal 的字面意思是:“当创建了一个 new Rabbit 时,把它的 [Prototype](Prototype.md) 赋值为 animal”。、
let animal = {
eats: true
};
function Rabbit(name) {
this.name = name;
}
Rabbit.prototype = animal;
let rabbit = new Rabbit("White Rabbit"); // rabbit.__proto__ == animal
console.log(rabbit.eats);// true
function Rabbit2(name) {
this.name = name;
this.__proto__ = animal;
}
let rabbit2 = new Rabbit2('Dano');
console.log(rabbit2.eats);//true也就是说,F.prototype=obj就是在F(){this.proto=obj}的效果是一样的。 
F.prototype 属性仅在 new F 被调用时使用,它为新对象的 [Prototype](Prototype.md) 赋值。
如果在创建之后,F.prototype 属性有了变化(F.prototype = <another object>),那么通过 new F 创建的新对象也将随之拥有新的对象作为 [Prototype](Prototype.md),但已经存在的对象将保持旧有的值。就是不会动态的改变,这符合常识。
每个函数都有 "prototype" 属性,即使我们没有提供它。 默认的 "prototype" 是一个只有属性 constructor 的对象,属性 constructor 指向函数自身。
……JavaScript 自身并不能确保正确的 "constructor" 函数值。
是的,它存在于函数的默认 "prototype" 中,但仅此而已。之后会发生什么 —— 完全取决于我们。 特别是,如果我们将整个默认 prototype 替换掉,那么其中就不会有 "constructor" 了。
就是说,每个函数都会有一个默认的prototype属性,这个属性的值是个对象(object),这个对象里面有一个constructor属性,值为构造函数F.prototype = {constructor: F}
通过这一点我们可以使用现在已经有的对象来构造一个对象:
function Rabbit(name) {
this.name = name;
alert(name);
}
let rabbit = new Rabbit("White Rabbit");
let rabbit2 = new rabbit.constructor("Black Rabbit");
一切都很简单,只需要记住几条重点就可以清晰地掌握了:
F.prototype属性(不要把它与[Prototype](Prototype.md)弄混了)在new F被调用时为新对象的[Prototype](Prototype.md)赋值。F.prototype的值要么是一个对象,要么就是null:其他值都不起作用。"prototype"属性仅当设置在一个构造函数上,并通过new调用时,才具有这种特殊的影响。
在常规对象上,prototype 没什么特别的:
let user = {
name: "John",
prototype: "Bla-bla" // 这里只是普通的属性
};function Rabbit() {}
Rabbit.prototype = {
eats: true
};
let rabbit = new Rabbit();
alert( rabbit.eats ); // truefunction Rabbit() {}
Rabbit.prototype = {
eats: true
};
let rabbit = new Rabbit();
Rabbit.prototype = {};
alert( rabbit.eats ); // ?答案是true,应为一但rabbit构造出来它的prototype的引用所指向的对象就不会变了,而对象本身还是可以改变的这和const声明一个对象很像:
function Rabbit() {}
Rabbit.prototype = {
eats: true
};
let rabbit = new Rabbit();
Rabbit.prototype.eats = false;
alert( rabbit.eats ); // ?在这里,直接修改的对象里面的属性的值,而不是修改整个对象,就会成功。
原生的原型
let obj = {};
alert( obj ); //[object Object]简短的表达式 obj = {} 和 obj = new Object() 是一个意思,其中 Object 就是一个内建的对象构造函数,其自身的 prototype 指向一个带有 toString 和其他方法的一个巨大的对象。
当 new Object() 被调用(或一个字面量对象 {...} 被创建),按照前面章节中我们学习过的规则,这个对象的 [Prototype](Prototype.md) 属性被设置为 Object.prototype
就是说,Object()这个函数的prototype属性指向一个巨大的对象,这和之前的:
let human = {
age: 21,
}
function Users(name) {
this.name = name;
console.log(`obj ${name} created!!!`);
}
Users.prototype = human;
let users = new Users('dano');
console.log(users.__proto__);的形式很相似。
也就是说,这个Object构造函数的prototype被那个巨大的对象赋值
let obj = {};
alert(obj.__proto__ === Object.prototype); // true
alert(obj.toString === obj.__proto__.toString); //true
alert(obj.toString === Object.prototype.toString); //true其他内建对象,像 Array、Date、Function 及其他,都在 prototype 上挂载了方法。
例如,当我们创建一个数组 [1, 2, 3],在内部会默认使用 new Array() 构造器。因此 Array.prototype 变成了这个数组的 prototype,并为这个数组提供数组的操作方法。这样内存的存储效率是很高的。
所有的内建原型顶端都是 Object.prototype。这就是为什么有人说“一切都从对象继承而来”。
一些方法在原型上可能会发生重叠,例如,Array.prototype 有自己的 toString 方法来列举出来数组的所有元素并用逗号分隔每一个元素。正如我们之前看到的那样,Object.prototype 也有 toString 方法,但是 Array.prototype 在原型链上更近,所以数组对象原型上的方法会被使用。 
基本数据类型,它们并不是对象。但是如果我们试图访问它们的属性,那么临时包装器对象将会通过内建的构造器 String、Number 和 Boolean 被创建。它们提供给我们操作字符串、数字和布尔值的方法然后消失。
这些对象对我们来说是无形地创建出来的。大多数引擎都会对其进行优化,但是规范中描述的就是通过这种方式。这些对象的方法也驻留在它们的 prototype 中,可以通过 String.prototype、Number.prototype 和 Boolean.prototype 进行获取。
特殊值 null 和 undefined 比较特殊。它们没有对象包装器,所以它们没有方法和属性。并且它们也没有相应的原型。
原生的原型是可以被修改的。例如,我们向 String.prototype 中添加一个方法,这个方法将对所有的字符串都是可用的。在开发的过程中,我们可能会想要一些新的内建方法,并且想把它们添加到原生原型中。但这通常是一个很不好的想法。
在现代编程中,只有一种情况下允许修改原生原型。那就是 polyfilling(垫片)。
Polyfilling 是一个术语,表示某个方法在 JavaScript 规范中已存在,但是特定的 JavaScript 引擎尚不支持该方法,那么我们可以通过手动实现它,并用以填充内建原型。
从原型中借用函数:
let obj = {
0: "Hello",
1: "world!",
length: 2,
};
obj.join = Array.prototype.join;
console.log(obj.join(',')); // Hello,world!上面这段代码有效,是因为内建的方法 join 的内部算法只关心正确的索引和 length 属性。它不会检查这个对象是否是真正的数组。许多内建方法就是这样。
另一种方式是通过将 obj.__proto__ 设置为 Array.prototype,这样 Array 中的所有方法都自动地可以在 obj 中使用了。
但是如果 obj 已经从另一个对象进行了继承,那么这种方法就不可行了(译注:因为这样会覆盖掉已有的继承。此处 obj 其实已经从 Object 进行了继承,但是 Array 也继承自 Object,所以此处的方法借用不会影响 obj 对原有继承的继承,因为 obj 通过原型链依旧继承了 Object)。请记住,我们一次只能继承一个对象。
原型方法和没有__proto__的对象
使用 obj.__proto__ 设置或读取原型被认为已经过时且不推荐使用(deprecated)了(已经被移至 JavaScript 规范的附录 B,意味着仅适用于浏览器)。
现代的获取/设置原型的方法有:
- Object.getPrototypeOf(obj) —— 返回对象 obj 的 Prototype。
- Object.setPrototypeOf(obj, proto) —— 将对象 obj 的 Prototype 设置为 proto。
__proto__ 不被反对的唯一的用法是在创建新对象时,将其用作属性:{ __proto__: ... }
Object.create(proto, [descriptors]) —— 利用给定的 proto 作为 Prototype 和可选的属性描述来创建一个空对象。
let animal = {
eats: true,
};
let rabbit = Object.create(animal);
console.log(rabbit.eats); //true
console.log(Object.getPrototypeOf(rabbit) === animal);//true
Object.setPrototypeOf(rabbit, {});
console.log(rabbit.eats);//undefinedObject.create 方法更强大,因为它有一个可选的第二参数:属性描述器 描述器的格式与 属性标志和属性描述符 一章中所讲的一样:
let animal = {
eats: true,
};
let rabbit = Object.create(animal, {
likes: {
value:true,
}
});
console.log(rabbit.likes);//true
console.log(Object.getOwnPropertyDescriptors(rabbit));
/**
* {
likes: {
value: true,
writable: false,
enumerable: false,
configurable: false
}
}
*/
console.log(rabbit.eats);//true我们可以使用 Object.create 来实现比复制 for..in 循环中的属性更强大的对象克隆方式:
let clone = Object.create(
Object.getPrototypeOf(obj),//原型
Object.getOwnPropertyDescriptors(obj)//属性描述
);是浅拷贝。
从技术上来讲,我们可以在任何时候 get/set [Prototype](Prototype.md)。但是通常我们只在创建对象的时候设置它一次,自那之后不再修改:rabbit 继承自 animal,之后不再更改。
并且,JavaScript 引擎对此进行了高度优化。用 Object.setPrototypeOf 或 obj.__proto__= “即时”更改原型是一个非常缓慢的操作,因为它破坏了对象属性访问操作的内部优化。因此,除非你知道自己在做什么,或者 JavaScript 的执行速度对你来说完全不重要,否则请避免使用它。
let obj = {};
let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";
alert(obj[key]); // [object Object],并不是 "some value"!这里如果用户输入 __proto__,那么在第四行的赋值会被忽略!
对于非开发者来说,这肯定很令人惊讶,但对我们来说却是可以理解的。__proto__ 属性很特殊:它必须是一个对象或者 null。字符串不能成为原型。这就是为什么将字符串赋值给 __proto__ 会被忽略。
通常开发者完全不会考虑到这一点。这让此类 bug 很难被发现,甚至变成漏洞,尤其是在 JavaScript 被用在服务端的时候。
现在,我们想要将一个对象用作关联数组,并且摆脱此类问题,我们可以使用一些小技巧:
let obj = Object.create(null);
// 或者:obj = { __proto__: null }
let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";
alert(obj[key]); // "some value"Object.create(null) 创建了一个空对象,这个对象没有原型([Prototype](Prototype.md) 是 null)
因此,它没有继承 __proto__ 的 getter/setter 方法。现在,它被作为正常的数据属性进行处理,因此上面的这个示例能够正常工作。
我们可以把这样的对象称为 “very plain” 或 “pure dictionary” 对象,因为它们甚至比通常的普通对象(plain object){...} 还要简单。
大多数与对象相关的方法都是 Object.something(...),例如 Object.keys(obj) —— 它们不在 prototype 中,因此在 “very plain” 对象中它们还是可以继续使用:
let chineseDictionary = Object.create(null);
chineseDictionary.hello = "你好";
chineseDictionary.bye = "再见";
alert(Object.keys(chineseDictionary)); // hello,byefunction Rabbit(name) {
this.name = name;
}
Rabbit.prototype.sayHi = function () {
console.log(this.name);
};
let rabbit = new Rabbit("Rabbit");
rabbit.sayHi();//Rabbit
Rabbit.prototype.sayHi();//undefined
Object.getPrototypeOf(rabbit).sayHi();//undefined
rabbit.__proto__.sayHi();//undefined除了第一个的this是rabbit,剩下的都是Rabbit.prototype,所以都是undefined