js 原型和原型链

143 阅读4分钟

js 原型及原型链梳理。

一 原型/原型链

概念说明
原型保存所有子对象的共有属性和方法的对象
原型链由各级子对象的__proto__属性连续引用形成的结构

示例代码:

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.run = function(){
        console.log(name + 'is running');
    }
}

1. 原型

原型,当创建一个函数时,该函数会挂载一个prototype属性,并且可以在该属性上定义属性和方法,如: Person.prototype.weight = '60kg'prototype中的内容被所有的子类继承,原型的最大作用就是“继承”。 而 prototype 本身又会有constructor属性,该属性指向构造函数本身:Person.prototype.constructor === Person

2. 原型链

对构造函数进行实例化:var p = new Person('jack', 20),p 的__proto__属性指向构造函数的原型: p.__proto__ === Person.prototype。以__proto__属性连续引用形成的结构就成为“原型链”。。所有对象都有__proto__属性,指向其构造函数的原型。

3. 示意图

prototype.png

所有对象都有__proto__属性,指向对象的原型。 Function 是顶层构造器,Object 是顶层对象,最顶层是 Null。从原型上来说,Function 继承了 Object; 从构造器来说,Function 构造了 Object。

prototype2.png

二 与原型有关的方法

方法说明
Object.create()从原型对象生成新的实例对象,接受两个参数,第一个是原型对象,第二个是描述符
Object.getPrototypeOf()获取对象的原型
Object.setPrototypeOf()为现有对象设置原型,返回一个新对象。接收两个参数,第一个是现有对象,第二个是新设置的原型对象
isPrototypeOf判断指定对象是否存在于另一个对象的原型链中,返回Boolean值
hasOwnProperty判断对象是否存在某个自有属性,返回Boolean值

注意: 使用 for...in 的时候,记得使用 hasOwnProperty,以防止遍历出原型链上的属性。

三 QUESTIONS

question 1: 搜索算法

给定以下对象:

let head = {
  glasses: 1
};

let table = {
  pen: 3
};

let bed = {
  sheet: 1,
  pillow: 2
};

let pockets = {
  money: 2000
};

1). 使用 proto 来分配原型,以使得任何属性的查找都遵循以下路径:pockets → bed → table → head。例如,pockets.pen 应该是 3(在 table 中找到),bed.glasses 应该是 1(在 head 中找到)。

2). 回答问题:通过 pockets.glasses 或 head.glasses 获取 glasses,哪个更快?必要时需要进行基准测试。

答案:

  1. 添加 proto:
let head = {
  glasses: 1
};

let table = {
  pen: 3,
  __proto__: head
};

let bed = {
  sheet: 1,
  pillow: 2,
  __proto__: table
};

let pockets = {
  money: 2000,
  __proto__: bed
};
  1. 在现代引擎中,从性能的角度来看,我们是从对象还是从原型链获取属性都是没区别的。引擎会记住在哪里找到的该属性,并在下一次请求中重用它。例如,对于 pockets.glasses 来说,引擎会记得在哪里找到的 glasses(在 head 中),这样下次就会直接在这个位置进行搜索。并且引擎足够聪明,一旦有内容更改,它们就会自动更新内部缓存,因此,该优化是安全的。

question 2: 为什么两只仓鼠都饱了?

我们有两只仓鼠:speedy 和 lazy 都继承自普通的 hamster 对象。当我们喂其中一只的时候,另一只也吃饱了。为什么?如何修复它?

let hamster = {
  stomach: [],
  eat(food) {
    this.stomach.push(food);
  }
};

let speedy = {
  __proto__: hamster
};

let lazy = {
  __proto__: hamster
};

// 这只仓鼠找到了食物
speedy.eat("apple");
console.log( speedy.stomach ); // apple

// 这只仓鼠也找到了食物,为什么?请修复它。
console.log( lazy.stomach ); // apple

分析及答案:

在调用 speedy.eat("apple") 的时候,发生了什么?

1). speedy.eat 方法在原型(hamster)中被找到,然后将 this 指向 speedy。

2). this.stomach.push() 需要找到 stomach 属性,然后对其调用 push。它在 this(speedy)中查找 stomach,但并没有找到。

3). 然后它顺着原型链,在 hamster 中找到 stomach。

4). 然后它对 stomach 调用 push,将食物添加到 stomach 的原型中。

因此,所有的仓鼠共享了同一个胃!对于 lazy.stomach.push() 和 speedy.stomach.push() 而言,属性 stomach 被在原型中找到(不是在对象自身),然后向其中 push 了新数据。

请注意,在简单的赋值 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 对象中,也就是直接写入到 speedy 和 lazy 对象中,给 speedy 和 lazy 增加了 stomach 属性。

此外,我们还可以通过确保每只仓鼠都有自己的胃来完全回避这个问题:

let hamster = {
  stomach: [],
  eat(food) {
    this.stomach.push(food);
  }
};

let speedy = {
  __proto__: hamster,
  stomach: []
};

let lazy = {
  __proto__: hamster,
  stomach: []
};

// 仓鼠 Speedy 找到了食物
speedy.eat("apple");
alert( speedy.stomach ); // apple

// 仓鼠 Lazy 的胃是空的
alert( lazy.stomach ); // <nothing>