原型继承

编程中希望能扩展或复用一些东西,JS中的原型继承这个语言特性可以实现这一个需求。

[[Property]]

一个内部隐藏属性,要么是 null(Object.prototype.proto或Object.create(null)) ,要么是另一个对象的引用。
image.png

设置原型和获取原型的方式

使用 __proto__ 来获取,或设置。

  1. let animal = {eats: true}
  2. let rabbit = {jumps: true}
  3. rabbit.__proto__ = animal
  4. console.log(rabbit.__proto__) // animal

效果

访问rabbit上不存在的属性或方法,会顺着其原型对象找到animal身上。这里我们说animal是rabbit的原型。
原型链可以很长。

本质

proto是[[Prototype]] 的因历史原因而留下来的 getter/setter。现代语言建议使用 Object.getPropertyOf(obj) & Object.setPropertyOf(obj) 取代使用 __proto__ ,但规范要求支持 __proto__ 因此使用它是安全的。

注意:

  1. 不能在闭环中分配 **__proto__**
  2. proto只能设置对象和null,其他会忽略

**

读取属性使用原型,写入属性不使用原型

写入和删除,都是直接在操作的对象本身进行的。

  1. let animal = {
  2. walk() {console.log('animal walk')}
  3. }
  4. let rabbit = {
  5. __proto__: animal
  6. }
  7. rabbit.walk = function(){console.log('rabbit walk')}
  8. rabbit.walk() // rabbit walk

但访问器属性是例外(因为本质是函数啊!所以实际上是调用了set函数)

  1. let user = {
  2. name: "John",
  3. surname: "Smith",
  4. set fullName(value) {
  5. [this.name, this.surname] = value.split(" ");
  6. },
  7. get fullName() {
  8. return `${this.name} ${this.surname}`;
  9. }
  10. };
  11. let admin = {
  12. __proto__: user,
  13. isAdmin: true
  14. };
  15. alert(admin.fullName); // John Smith (*)
  16. // setter triggers!
  17. admin.fullName = "Alice Cooper"; // 这里调用了user的set fullName
  18. alert(admin.fullName);
  19. alert(user.fullName);

this的指向

上例中的this,指向的是user还是admin呢?
注意,this不受原型影响,只关注this前的符号.

  1. let animal = {
  2. walk() {console.log('animal walk')}
  3. eat() {this.eating = true}
  4. }
  5. let rabbit = {
  6. __proto__: animal
  7. }
  8. rabbit.eat() // rabbit.eating = true animal.eating = undefined

for in

遍历对象属性,也会迭代继承的属性,如果属性是不可迭代(enumerable: false),则无法读取。

如果只想获取对象自身的属性,可以通过 getOwnProperty 来进行判断。

几乎所有其他的键/值获取方法都忽略继承的属性,比如Object.keys | values等。

练习

注意查找还是赋值

  1. let hamster = {
  2. stomach: [],
  3. eat(food) {
  4. this.stomach.push(food); // *
  5. }
  6. };
  7. let speedy = {
  8. __proto__: hamster
  9. };
  10. let lazy = {
  11. __proto__: hamster
  12. };
  13. // 这只仓鼠找到了食物
  14. speedy.eat("apple");
  15. alert( speedy.stomach ); // apple
  16. // 这只仓鼠也找到了食物,为什么?请修复它。
  17. alert( lazy.stomach ); // apple

*号这里要注意this.stomach.push 会沿着原型链去查找 stomach 属性,而不是给this对象去赋值。因此题目里会沿着原型链查找到 hamster 身上。

F.prototype

通过 new F() 调用构造函数可以创建一个新的对象。如果 F.prototype 是一个对象,则 new 操作符会使用它为新的对象(实例)设置 [[Prototype]]

JS从语言设计之初就有原型继承,但过去缺少访问它的方式,唯一可靠的方式是访问,构造函数的** **prototype 属性。

只是常规(普通)属性

从单词字面理解,跟原型很像,但实际上只是一个常规的属性,可以修改。

  1. let animal = {
  2. eats: true
  3. };
  4. function Rabbit(name) {
  5. this.name = name;
  6. }
  7. Rabbit.prototype = animal // 在声明这句话前,Rabbit其实已经有了prototype属性,是一个对象,具有constructor属性,指向构造函数自身
  8. let rabbit = new Rabbit('white'); // 此时构造出的实例,原型指向的是animal对象了。
  9. rabbit.eats // trueA

image.png

  1. F.prototype仅仅在函数作为构造函数调用,即new F()时,才会使用它,将其作为实例对象的[[prototype]]的值。
  2. F.prototype可以随时更改,更改后创建的实例拥有最新的prototype属性值作为其[[prototype]]的值,而之前创建的实例,[[prototype]]保持原有旧值

默认的prototype

每个函数在声明后,都会有一个 prototype 属性,默认是一个对象,包含 constructor 属性,指向函数自身。

  1. function Rabbit() {}
  2. /* default prototype
  3. Rabbit.prototype = { constructor: Rabbit };
  4. */

image.png
通常不作修改原型的操作,我们可以判断实例来自谁

  1. let rabbit = new Rabbit() // prototype = {constructor: Rabbit}
  2. rabbit.constructor == Rabbit // true,访问的是其原型上的constructor
  3. // 所以我们还可以这么干
  4. let rabbit2 = new rabbit.construtor()

JS不能确保正确的constructor值

因为我们可以覆盖式重写 prototype

  1. function Rabbit() {}
  2. Rabbit.prototype = {
  3. jumps: true
  4. };
  5. let rabbit = new Rabbit();
  6. alert(rabbit.constructor === Rabbit); // false

所以正确的写法应该是添加和删除,而不是覆盖

  1. function Rabbit() {}
  2. // 不要将 Rabbit.prototype 整个覆盖
  3. // 可以向其中添加内容
  4. Rabbit.prototype.jumps = true

原生的原型

内置构造函数都用到了 prototype

Object.prototype

  1. let obj = {}
  2. alert(obj) // [object Object]
  3. // 这里之所以生成了字符串,是因为obj.prototype 指向的是内置Object的prototype对象,其包含很多方法,toString就是其中之一。

image.png

  1. obj.__proto__ === Object.prototype

注意:Object.prototype之上没有更多的原型了。

  1. Object.prototyoe.__proto__ === null

其他内建原型

像Array,Date,Function等等,都是在prototype上挂在了很多方法。
image.png

  1. let arr = []
  2. arr.__proto__ === Array.prototype
  3. Array.prototype.__proto__ === Object.prototype
  4. Object.prototype.__proto__ === null
  5. // 所以
  6. arr.__proto__.__proto__.__proto__ === null

注意:一些方法在原型上会有重叠,比如Array.prototype有自己的 **toString** 方法,这种情况,使用原型链查找中最近的方法。

  1. let arr = [1,2,3]
  2. arr.toString() // 1,2,3 调用Array.prototype.toString方法
  3. // 看下如果使用了Object.prototype.toString
  4. Object.prototype.toString.call(arr) // "[object Array]"

内置原生原型可修改

  1. String.prototype.show = function() {
  2. alert(this);
  3. };
  4. "BOOM!".show(); // BOOM!

因此很容易覆盖,冲突。现代编程中,只有polyfilling才允许修改原型。

原型借用

一种灵活的技巧。

  1. let obj = {
  2. 0: "Hello",
  3. 1: "world!",
  4. length: 2,
  5. };
  6. // 因为数组方法要求不高,只要符合数字索引,有length,就可以像数组一样被使用
  7. obj.join = Array.prototype.join
  8. obj.join(',') // Hello,world

题目

给函数添加一个f.defer(ms)方法

  1. function f() {
  2. alert("Hello!");
  3. }
  4. f.defer(1000); // 1 秒后显示 "Hello!"
  5. Function.prototype.defer = function(ms) {
  6. const fn = this
  7. setTimeout(fn, ms)
  8. }

装饰器defer

  1. function f(a, b) {
  2. alert( a + b );
  3. }
  4. f.defer(1000)(1, 2); // 1 秒后显示 3
  5. Function.prototype.defer = function (ms) {
  6. const fn = this; // 拿到函数f,因为是f.defer,.前面就是this指向了,这里f是个函数,也就是我们的题目中的原始的需要真正执行的函数。
  7. return (...args) => {
  8. setTimeout(fn.bind(this, ...args), ms); // *
  9. };
  10. };

注意星号的this

原以为:这里fn.bind(this 中的this,可以保证适用于对象方法来调用。该题中,此时this = window,因为是f.defer(ms)得到了这个return的函数,这个函数是裸调用,因此是window。
~~
这里return的是箭头函数,因此没有自己的this,获取外层的,应该是f.defer(ms)(1,2)调用,因此是f,而且是bind绑定,this的优先级较高(下面变体解释会用到)

但如果将箭头函数改为普通匿名函数,则指向this。

  1. Function.prototype.defer = function (ms) {
  2. const fn = this;
  3. return function (...args) {
  4. console.log(this);
  5. setTimeout(fn.bind(this, ...args), ms);
  6. };
  7. };
  8. f.defer(1000)(1, 2)// f.defer(ms) 拿到return的函数,然后裸调用,指向window。

如果需要适应对象方法调用,箭头函数的写法就有问题

  1. Function.prototype.defer = function (ms) {
  2. const fn = this;
  3. return (...args) => {
  4. console.log(this)
  5. setTimeout(fn.bind(this, ...args), ms); // *
  6. };
  7. };
  8. let user = {
  9. name: "John",
  10. sayHi() {
  11. alert(this.name);
  12. }
  13. }
  14. user.sayHi = user.sayHi.defer(1000)
  15. user.sayHi() // 这里看起来是对象.方法调用,this应该是user,打印John。但!!!不是这样的,注意看星号代码,通过bind绑定的this,且箭头函数无this,获取外层的,user.sayHi.defer,看defer前的对象,是sayHi,因此这里bind的this是sayHi。打印看看结果: sayHi
  16. 因为alert调用了函数自身的toString(),打印了函数自身

原型方法,无proto的对象

__proto__ 是以前为了方便获取和设置原型而存在的一个 setter,getter访问器属性,JS规范并不推荐,建议使用如下:

  • Object.create(proto, [descriptors]),第二个参数是可选的属性描述。
  • Object.setPrototypeOf(obj, proto)
  • Object.getPrototypeOf(obj)

    一种强大的拷贝方式:

    1. const clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))

    原型历史

  1. 函数的 prototype 一直都存在,从声明开始就有。
  2. 12年, Object.create 出现在标准里,但仅能创建,没有提供 set/get 的能力,浏览器厂商自己实现了非标准的 __proto__ 访问器,允许用户随时 get/set
  3. 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. 1. 你可以采用 `Map` 来代替普通对象存储
  2. 1. 使用纯字典|标准对象 `Object.create(null)` ,创建一个无原型对象。
  3. <a name="ap0ZL"></a>
  4. #### **注意:**
  5. 上述第二个方法也有一点小缺点,无原型,说明无原型上的方法可供使用,如toString
  6. ```javascript
  7. let obj = Object.create(null)
  8. obj.toString() // obj.toString is not a function

但对象的大部分实用方法,还是在Object对象上的,可以继续使用

  1. Object.keys(obj) // []

练习

  1. 添加原型方法,使其工作 ```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

  1. ```javascript
  2. let dictionary = Object.create(null, {
  3. toString: {
  4. // 其他标识符默认为false,不用管。
  5. // 回顾下,如果是对象字面量形式书写,默认值都是true了。
  6. value() {
  7. return Object.keys(this).join()
  8. }
  9. }
  10. })