原型继承
编程中希望能扩展或复用一些东西,JS中的原型继承这个语言特性可以实现这一个需求。
[[Property]]
一个内部隐藏属性,要么是 null
(Object.prototype.proto或Object.create(null)) ,要么是另一个对象的引用。
设置原型和获取原型的方式
使用 __proto__
来获取,或设置。
let animal = {eats: true}
let rabbit = {jumps: true}
rabbit.__proto__ = animal
console.log(rabbit.__proto__) // animal
效果
访问rabbit上不存在的属性或方法,会顺着其原型对象找到animal身上。这里我们说animal是rabbit的原型。
原型链可以很长。
本质
proto是[[Prototype]] 的因历史原因而留下来的 getter/setter。现代语言建议使用 Object.getPropertyOf(obj) & Object.setPropertyOf(obj)
取代使用 __proto__
,但规范要求支持 __proto__
因此使用它是安全的。
注意:
- 不能在闭环中分配 **
__proto__
** - proto只能设置对象和null,其他会忽略
读取属性使用原型,写入属性不使用原型
写入和删除,都是直接在操作的对象本身进行的。
let animal = {
walk() {console.log('animal walk')}
}
let rabbit = {
__proto__: animal
}
rabbit.walk = function(){console.log('rabbit walk')}
rabbit.walk() // rabbit walk
但访问器属性是例外(因为本质是函数啊!所以实际上是调用了set函数)
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
};
alert(admin.fullName); // John Smith (*)
// setter triggers!
admin.fullName = "Alice Cooper"; // 这里调用了user的set fullName
alert(admin.fullName);
alert(user.fullName);
this的指向
上例中的this,指向的是user还是admin呢?
注意,this不受原型影响,只关注this前的符号.
let animal = {
walk() {console.log('animal walk')}
eat() {this.eating = true}
}
let rabbit = {
__proto__: animal
}
rabbit.eat() // rabbit.eating = true animal.eating = undefined
for in
遍历对象属性,也会迭代继承的属性,如果属性是不可迭代(enumerable: false),则无法读取。
如果只想获取对象自身的属性,可以通过 getOwnProperty
来进行判断。
几乎所有其他的键/值获取方法都忽略继承的属性,比如Object.keys | values等。
练习
注意查找还是赋值
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
*号这里要注意, this.stomach.push
会沿着原型链去查找 stomach
属性,而不是给this对象去赋值。因此题目里会沿着原型链查找到 hamster
身上。
F.prototype
通过 new F()
调用构造函数可以创建一个新的对象。如果 F.prototype
是一个对象,则 new
操作符会使用它为新的对象(实例)设置 [[Prototype]]
JS从语言设计之初就有原型继承,但过去缺少访问它的方式,唯一可靠的方式是访问,构造函数的** **
prototype
属性。
只是常规(普通)属性
从单词字面理解,跟原型很像,但实际上只是一个常规的属性,可以修改。
let animal = {
eats: true
};
function Rabbit(name) {
this.name = name;
}
Rabbit.prototype = animal // 在声明这句话前,Rabbit其实已经有了prototype属性,是一个对象,具有constructor属性,指向构造函数自身
let rabbit = new Rabbit('white'); // 此时构造出的实例,原型指向的是animal对象了。
rabbit.eats // trueA
- F.prototype仅仅在函数作为构造函数调用,即new F()时,才会使用它,将其作为实例对象的[[prototype]]的值。
- F.prototype可以随时更改,更改后创建的实例拥有最新的prototype属性值作为其[[prototype]]的值,而之前创建的实例,[[prototype]]保持原有旧值。
默认的prototype
每个函数在声明后,都会有一个 prototype
属性,默认是一个对象,包含 constructor
属性,指向函数自身。
function Rabbit() {}
/* default prototype
Rabbit.prototype = { constructor: Rabbit };
*/
通常不作修改原型的操作,我们可以判断实例来自谁
let rabbit = new Rabbit() // prototype = {constructor: Rabbit}
rabbit.constructor == Rabbit // true,访问的是其原型上的constructor
// 所以我们还可以这么干
let rabbit2 = new rabbit.construtor()
JS不能确保正确的constructor值
因为我们可以覆盖式重写 prototype
function Rabbit() {}
Rabbit.prototype = {
jumps: true
};
let rabbit = new Rabbit();
alert(rabbit.constructor === Rabbit); // false
所以正确的写法应该是添加和删除,而不是覆盖
function Rabbit() {}
// 不要将 Rabbit.prototype 整个覆盖
// 可以向其中添加内容
Rabbit.prototype.jumps = true
原生的原型
Object.prototype
let obj = {}
alert(obj) // [object Object]
// 这里之所以生成了字符串,是因为obj.prototype 指向的是内置Object的prototype对象,其包含很多方法,toString就是其中之一。
obj.__proto__ === Object.prototype
注意:Object.prototype之上没有更多的原型了。
Object.prototyoe.__proto__ === null
其他内建原型
像Array,Date,Function等等,都是在prototype上挂在了很多方法。
let arr = []
arr.__proto__ === Array.prototype
Array.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null
// 所以
arr.__proto__.__proto__.__proto__ === null
注意:一些方法在原型上会有重叠,比如Array.prototype有自己的 **toString
** 方法,这种情况,使用原型链查找中最近的方法。
let arr = [1,2,3]
arr.toString() // 1,2,3 调用Array.prototype.toString方法
// 看下如果使用了Object.prototype.toString
Object.prototype.toString.call(arr) // "[object Array]"
内置原生原型可修改
String.prototype.show = function() {
alert(this);
};
"BOOM!".show(); // BOOM!
因此很容易覆盖,冲突。现代编程中,只有polyfilling才允许修改原型。
原型借用
一种灵活的技巧。
let obj = {
0: "Hello",
1: "world!",
length: 2,
};
// 因为数组方法要求不高,只要符合数字索引,有length,就可以像数组一样被使用
obj.join = Array.prototype.join
obj.join(',') // Hello,world
题目
给函数添加一个f.defer(ms)方法
function f() {
alert("Hello!");
}
f.defer(1000); // 1 秒后显示 "Hello!"
Function.prototype.defer = function(ms) {
const fn = this
setTimeout(fn, ms)
}
装饰器defer
function f(a, b) {
alert( a + b );
}
f.defer(1000)(1, 2); // 1 秒后显示 3
Function.prototype.defer = function (ms) {
const fn = this; // 拿到函数f,因为是f.defer,.前面就是this指向了,这里f是个函数,也就是我们的题目中的原始的需要真正执行的函数。
return (...args) => {
setTimeout(fn.bind(this, ...args), ms); // *
};
};
注意星号的this
原以为:这里fn.bind(this 中的this,可以保证适用于对象方法来调用。该题中,此时this = window,因为是f.defer(ms)得到了这个return的函数,这个函数是裸调用,因此是window。
~~
这里return的是箭头函数,因此没有自己的this,获取外层的,应该是f.defer(ms)(1,2)调用,因此是f,而且是bind绑定,this的优先级较高(下面变体解释会用到)
但如果将箭头函数改为普通匿名函数,则指向this。
Function.prototype.defer = function (ms) {
const fn = this;
return function (...args) {
console.log(this);
setTimeout(fn.bind(this, ...args), ms);
};
};
f.defer(1000)(1, 2)// f.defer(ms) 拿到return的函数,然后裸调用,指向window。
如果需要适应对象方法调用,箭头函数的写法就有问题
Function.prototype.defer = function (ms) {
const fn = this;
return (...args) => {
console.log(this)
setTimeout(fn.bind(this, ...args), ms); // *
};
};
let user = {
name: "John",
sayHi() {
alert(this.name);
}
}
user.sayHi = user.sayHi.defer(1000)
user.sayHi() // 这里看起来是对象.方法调用,this应该是user,打印John。但!!!不是这样的,注意看星号代码,通过bind绑定的this,且箭头函数无this,获取外层的,user.sayHi.defer,看defer前的对象,是sayHi,因此这里bind的this是sayHi。打印看看结果: sayHi
因为alert调用了函数自身的toString(),打印了函数自身
原型方法,无proto的对象
__proto__
是以前为了方便获取和设置原型而存在的一个 setter,getter访问器属性,JS规范并不推荐,建议使用如下:
- Object.create(proto, [descriptors]),第二个参数是可选的属性描述。
- Object.setPrototypeOf(obj, proto)
- Object.getPrototypeOf(obj)
一种强大的拷贝方式:
const clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))
原型历史
- 函数的
prototype
一直都存在,从声明开始就有。 - 12年,
Object.create
出现在标准里,但仅能创建,没有提供set/get
的能力,浏览器厂商自己实现了非标准的__proto__
访问器,允许用户随时get/set
- 15年,
Object.setPrototype|getPrototypeOf
加入到标准,但__proto__
已经加入到JS标准附件中,在所有环境基本上都是支持的。使用标准,而非proto
因为proto其实是访问器属性,setter内部做了判断,仅支持null和对象,不支持字符串等,所以赋值无效,这很容易出现隐藏BUG,难以发现。 ```javascript let obj = {};
let key = prompt(“What’s the key?”, “proto“); obj[key] = “some value”;
alert(obj[key]); // [object Object],并不是 “some value”!
1. 你可以采用 `Map` 来代替普通对象存储
1. 使用纯字典|标准对象 `Object.create(null)` ,创建一个无原型对象。
<a name="ap0ZL"></a>
#### **注意:**
上述第二个方法也有一点小缺点,无原型,说明无原型上的方法可供使用,如toString。
```javascript
let obj = Object.create(null)
obj.toString() // obj.toString is not a function
但对象的大部分实用方法,还是在Object对象上的,可以继续使用
Object.keys(obj) // []
练习
- 添加原型方法,使其工作 ```javascript let dictionary = Object.create(null);
// 你的添加 dictionary.toString 方法的代码
// 添加一些数据 dictionary.apple = “Apple”; dictionary.proto = “test”; // 这里 proto 是一个常规的属性键
// 在循环中只有 apple 和 proto for(let key in dictionary) { alert(key); // “apple”, then “proto“ }
// 你的 toString 方法在发挥作用 alert(dictionary); // “apple,proto“
```javascript
let dictionary = Object.create(null, {
toString: {
// 其他标识符默认为false,不用管。
// 回顾下,如果是对象字面量形式书写,默认值都是true了。
value() {
return Object.keys(this).join()
}
}
})