# 原型继承[对象]
# 基本用法
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // 设置 rabbit.[[Prototype]] = animal
现在,如果我们从 rabbit
中读取一个它没有的属性,JavaScript 会自动从 animal
中获取
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // (*)
// 现在这两个属性我们都能在 rabbit 中找到:
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true
# 继承后覆盖
假设有如下代码
let animal = {
eats: true,
sleep() {
console.log("animal sleep");
}
};
let rabbit = {
jumps: true,
__proto__: animal,
};
rabbit.sleep = function () {
console.log("rabbir sleep");
}
rabbit.sleep() // rabbir sleep
animal.sleep() // animal sleep
修改继承来的属性,并不会影响到原型链上的其他对象的属性
# ❗关于 this
让我们稍微修改上面的代码
let animal = {
eats: true,
sound:"wuwuuu", // 增加 sound
sleep() {
console.log(`animal sleep ${this.sound}` );
}
};
let rabbit = {
jumps: true,
__proto__: animal,
};
rabbit.sleep = function () {
console.log(`rabbir sleep ${this.sound}`);
}
rabbit.sleep() // rabbir sleep wuwuuu
animal.sleep() // animal sleep wuwuuu
会发现, rabbit.sleep
指向的 this
看似指向 animal 里的 this
,实际上,**无论在哪里找到方法:在一个对象还是在原型中。在一个方法调用中,this
始终是点符号 .
前面的对象。**所以这里的 this
一定指向的是 rabbit
。
再来看下这个:
let hamster = {
stomach: [],
eat(food) {
this.stomach.push(food);
}
};
let speedy = {
__proto__: hamster
};
let lazy = {
__proto__: hamster
};
// 这只仓鼠找到了食物
speedy.eat("apple");
alert( speedy.stomach ); // apple
// 这只仓鼠也找到了食物,为什么?请修复它。
alert( lazy.stomach ); // apple
❓为什么 speedy 和 lazy 的 stomach 中都有食物呢?首先方法执行的时候做了这些事:
speedy.eat("apple");
的时候,speedy
本身没有eat
那么会在原型链上找到
hamster
,然后执行eat
,接着回到this
(=speedy
)中查找stomach
但是
this
中并没有stomach
,然后它对stomach
调用push
显然会报错,那么就会去找原型链上的stomach
, 将食物添加到原型hamster
的stomach
中
对于 lazy.stomach.push(...)
和 speedy.stomach.push()
而言,属性 stomach
被在原型中找到(不是在对象自身),然后向其中 push
了新数据。
在简单的赋值 this.stomach=
的情况下不会出现这种情况,因为直接赋值的时候,如果没有 stomach
,则会自动在 this
中创建一个 stomach
:
let hamster = {
stomach: [],
eat(food) {
// 分配给 this.stomach 而不是 this.stomach.push
this.stomach = [food];
}
};
let speedy = {
__proto__: hamster
};
let lazy = {
__proto__: hamster
};
// 仓鼠 Speedy 找到了食物
speedy.eat("apple");
alert( speedy.stomach ); // apple
// 仓鼠 Lazy 的胃是空的
alert( lazy.stomach ); // <nothing>
this.stomach=
不会执行对 stomach
的查找。该值会被直接写入 this
对象。
所以我们的结论是:第一个例子 的 animal
代表“方法存储”,rabbit
在使用其中的方法。所以,方法是共享的,但对象状态不是。
直接执行的父级方法里是直接对属性赋值**,不会改变父级 key 的 value,但是如果方法里**做其他操作会触发原型链上的查找,那么会针对改变的对象就是父级,但是this始终是子级!
# 循环中会怎样
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
// Object.keys 只返回自己的 key
alert(Object.keys(rabbit)); // jumps
// for..in 会遍历自己以及继承的键
for(let prop in rabbit) alert(prop); // jumps,然后是 eats
rabbit.hasOwnProperty("eats") // false
rabbit.hasOwnProperty("jumps") // true
Object.keys
只会返回自己的 keys ,不会返回继承来的 key,但是 for...in
不管这些,都遍历。
几乎所有其他键/值获取方法,例如 Object.keys
和 Object.values
等,都会忽略继承的属性。
它们只会对对象自身进行操作。不考虑 继承自原型的属性。
如果我们想判断一个属性是继承来的还是自身有的,用 obj.hasOwnProperty(key) 来判断就好了。
# 现代应该使用的__proto__定义方法
__proto__
被认为是过时且不推荐使用的(deprecated),这里的不推荐使用是指 JavaScript 规范中规定,proto 必须仅在浏览器环境下才能得到支持。
现代的方法有:
- Object.create(proto, [descriptors]) —— 利用给定的
proto
作为[[Prototype]]
和可选的属性描述来创建一个空对象。 - Object.getPrototypeOf(obj) —— 返回对象
obj
的[[Prototype]]
。 - Object.setPrototypeOf(obj, proto) —— 将对象
obj
的[[Prototype]]
设置为proto
。
let animal = {
eats: true
};
// 创建一个以 animal 为原型的新对象
let rabbit = Object.create(animal);
//== let rabbit ={__proto__:animal}
alert(rabbit.eats); // true
alert(Object.getPrototypeOf(rabbit) === animal); // true
//== rabbit.__proto__ == animal
Object.setPrototypeOf(rabbit, {}); // 将 rabbit 的原型修改为 {}
//== rabbit.__proto__ = {}
Object.create
有一个可选的第二参数:属性描述器。我们可以在此处为新对象提供额外的属性,就像这样:
let animal = {
eats: true
};
let rabbit = Object.create(animal, {
jumps: {
value: true //除了 value 以外其他的属性参见【对象属性】文章
}
});
//== let rabbit = {jumps:true,__proto__:animal}
alert(rabbit.jumps); // true
描述器的格式与 属性标志和属性描述符 一章中所讲的一样。
我们可以使用 Object.create
来实现比复制 for..in
循环中的属性更强大的对象克隆方式:
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
上面的方法中,如果我们创建了一个对象 obj
,并且希望他有一个属性就叫做 __proto__
,那么我们需要使用 Object.create(null)
,这样他的 __proto__
就不会指向特定的对象
# F.prototype [函数作类继承]
# 基本使用
每个函数都有 prototype
属性,即使我们没有提供它。
默认的 "prototype"
是一个只有属性 constructor
的对象,属性 constructor
指向函数自身。
function Rabbit() {}
/* default prototype
Rabbit.prototype = { constructor: Rabbit };
*/
在这里,Rabbit.prototype 设置为 animal ,那么 rabbit 的 __proto__
就会被设置为 animal
let animal = {
eats: true
};
function Rabbit(name) {
this.name = name;
}
Rabbit.prototype = animal;
let rabbit = new Rabbit("White Rabbit"); // rabbit.__proto__ == animal
alert( rabbit.eats ); // true
我们在 new 一个新的对象时,可以使用继承的 constructor
,下面写法效果是一样的:
function Rabbit(name) {
this.name = name;
alert(name);
}
let rabbit = new Rabbit("White Rabbit");
let rabbit2 = new rabbit.constructor("Black Rabbit");
# 修改 prototype 会发生什么
- 这里直接把 Rabbit 的 prototype 直接整个换掉,这样的情况下,rabbit 实例的
__proto__
**并不会变化,而是仍然指向原来的对象 **{eat:xx}
,但是 Rabbit 的 prototype 指向完全变了
function Rabbit() {}
Rabbit.prototype = {
eats: true
};
let rabbit = new Rabbit();
Rabbit.prototype = {};
alert( rabbit.eats ); // true
console.log(rabbit.__proto__); // {eats: true}
- 这里我们结合上面的图,可以知道这里
Rabbit.prototype
的指向没变化,所以rabbit.__proto__
的eats
自然就是 false 了
function Rabbit() {}
Rabbit.prototype = {
eats: true
};
let rabbit = new Rabbit();
Rabbit.prototype.eats = false;
alert( rabbit.eats ); // false
console.log(rabbit.__proto__); // {eats: false}
- 我 rabbit 又没有 eats 属性,你删我的有啥用?当然还是 true 了
function Rabbit() {}
Rabbit.prototype = {
eats: true
};
let rabbit = new Rabbit();
delete rabbit.eats;
alert( rabbit.eats ); // true
- 这样删就对了嘛,你要删就去删我继承的人啊
function Rabbit() {}
Rabbit.prototype = {
eats: true
};
let rabbit = new Rabbit();
delete Rabbit.prototype.eats;
alert( rabbit.eats ); // undefined
constructor
是在 默认的prototype
里的,你把我的prototype
指向改了,我还怎么拿我的构造器,但是他会追根溯源找到constructor
为止,那么他一路就找到了Object
,最后其实是let user2 = new Object('Pete')
,这不是我们想要的结果。
function User(name) {
this.name = name;
}
User.prototype = {}; // (*)
let user = new User('John');
let user2 = new user.constructor('Pete');
alert( user2.name ); // undefined
# 实例、构造函数、原型的关系
function Person() { }
// Person.prototype 构造函数 -> 原型
Person.prototype.name = "p"
// 构造函数 -> 实例
let person = new Person()
// 原型 -> 构造函数
console.log(Person.prototype.constructor === Person); // true
// 实例 -> 原型
console.log(person.__proto__ === Person.prototype); //true
// 实例 -> 构造函数
console.log(person.__proto__.constructor===Person); //true
在读取一个实例的属性的过程中,如果属性在该实例中没有找到,那么就会循着 __proto__
指定的原型上去寻找,如果还找不到,则尝试寻找原型的原型
再看一个这个图:
我们用 let d = new Date()
来做例子
红色线的才是 d 的原型链!
Date 的原型是 Object 的实例,所以 Date.prototype.__proto__===Object.prototype
在构造函数层面:
// Object 是构造函数,所有构造函数都是 Function 的实例
Object.__proto__===Function.prototype
// Function 的原型又是 Object 的实例
Function.prototype.__proto__===Object.prototype
// Object 的原型显然不是 Object 的实例,他不继承于任何
Object.prototype.__proto__===null