JavaScript原型的那些事

整理自现代JavascriptMDN

原型与继承

对象有一个特殊的隐藏属性 [[Prototype]](如规范中所命名的),它要么为 null,要么就是对另一个对象的引用。该对象被称为“原型”

每个实例对象(object)都有一个私有属性(称之为 __proto__)指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(_proto_),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节

几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例

当我们从 object 中读取一个缺失的属性时,JavaScript 会自动从原型中获取该属性。在编程中,这种行为被称为“原型继承”

原型链

当访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾

[[Prototype]]

属性 [[Prototype]] 是内部的而且是隐藏的。但也有很多设置它的方式

  • 使用特殊的名字 __proto__

    __proto__[[Prototype]] 的因历史原因而留下来的 getter/setter,现代编程语言建议我们应该使用函数**Object.getPrototypeOf/Object.setPrototypeOf** 来取代 __proto__ 去 get/set 原型

    使用规则

    1. 引用不能形成闭环。如果我们试图在一个闭环中分配 __proto__,JavaScript 会抛出错误
    2. __proto__ 的值可以是对象,也可以是 null。而其他的类型都会被忽略
    let animal = {
      eats: true,
      walk() {
        alert("Animal walk");
      }
    };
    let rabbit = {
      jumps: true
    };
    
    rabbit.__proto__ = animal; // 设置 rabbit.[[Prototype]] = animal
    
    // 现在这两个属性我们都能在 rabbit 中找到:
    alert( rabbit.eats ); // true (**)
    alert( rabbit.jumps ); // true
    
    // walk 方法是从原型中获得的
    rabbit.walk(); // Animal walk
    
    /**
    	* 当 `alert` 试图读取 `rabbit.eats` `(**)` 时,因为它不存在于 `rabbit` 中,所以 JavaScript 会顺着 `[[Prototype]]` 引用,在 `animal` 中查找(自下而上)
    	*/
    
    
    // 原型链可以很长
    let longEar = {
      earLength: 10,
      __proto__: rabbit
    };
    // walk 是通过原型链获得的
    longEar.walk(); // Animal walk
    alert(longEar.jumps); // true(从 rabbit)
    
    /**
     ----------------------------
      animal:
    	eats: true
      	walk: function
      ----------------------------
                  ^
                  | [[Prototype]]
                  |
      ----------------------------
       rabbit:
       	jumps: true
      ----------------------------
                  ^
                  | [[Prototype]]
                  |
      ----------------------------
       longEar:
             earLength: 10
      ----------------------------
    */
    复制代码
写入不使用原型

原型仅用于读取属性。对于写入/删除操作可以直接在对象上进行

let animal = {
  eats: true,
  walk() {
    /* rabbit 不会使用此方法 */
  }
};

let rabbit = {
  __proto__: animal
};

rabbit.walk = function() {
  alert("Rabbit! Bounce-bounce!");
};

// rabbit.walk() 将立即在对象中找到该方法并执行,而无需使用原型
rabbit.walk(); // Rabbit! Bounce-bounce!

/**
 ----------------------------
 animal:
     eats: true
     walk: function
  ----------------------------
              ^
              |	[[Prototype]]
              |
  ----------------------------
   rabbit:
       walk: function
  ----------------------------
*/
复制代码
this的值

无论在哪里找到方法:在一个对象还是在原型中。在一个方法调用中,this 始终是点符号 . 前面的对象

方法是共享的,但对象状态不是

// animal 有一些方法
let animal = {
  walk() {
    if (!this.isSleeping) {
      alert(`I walk`);
    }
  },
  sleep() {
    this.isSleeping = true;
  }
};

let rabbit = {
  name: "White Rabbit",
  __proto__: animal
};

// 修改 rabbit.isSleeping
rabbit.sleep();

alert(rabbit.isSleeping); // true
alert(animal.isSleeping); // undefined(原型中没有此属性)

/**
 ----------------------------
 animal:
     walk: function
     sleep: function
  ----------------------------
              ^
              |	[[Prototype]]
              |
  ----------------------------
   rabbit:
       name: "White Rabbit"
  ----------------------------
*/
复制代码
for...in循环

for..in 循环也会迭代继承的、可枚举的属性

所有其他的键/值获取方法仅对对象本身起作用,忽略继承的属性

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
复制代码
  • 使用obj.hasOwnProperty(key)过滤继承属性
let animal = {
  eats: true
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

for(let prop in rabbit) {
  let isOwn = rabbit.hasOwnProperty(prop);

  if (isOwn) {
    alert(`Our: ${prop}`); // Our: jumps
  } else {
    alert(`Inherited: ${prop}`); // Inherited: eats
  }
}

/**
  --------------------------------
   null
  --------------------------------
              ^
              |	[[Prototype]]
              |
  --------------------------------
   Object.prototype:
       toString: function
       hasOwnProperty: function
       ...
  --------------------------------
              ^
              |	[[Prototype]]
              |
  --------------------------------
   animal:
   	eats: true
  --------------------------------
              ^
              | [[Prototype]]  // rabbit.__proto__ ==== animal
              |
  --------------------------------
   rabbit:
   	jumps: true
  --------------------------------
*/
复制代码
F.prototype

这里的 F.prototype 指的是 F 的一个名为 "prototype" 的常规属性(普通构造函数的prototype

  • F.prototype 属性(不要把它与 [[Prototype]] 弄混了)在 new F 被调用时为新对象的 [[Prototype]] 赋值
  • F.prototype 的值要么是一个对象,要么就是 null:其他值都不起作用
  • "prototype" 属性仅在设置了一个构造函数(constructor function),并通过 new 调用时,才具有这种特殊的影响
  • 如果 F.prototype 是一个对象,那么 new 操作符会使用它为新对象设置 [[Prototype]]

    let animal = {
      eats: true
    };
    
    function Rabbit(name) {
      this.name = name;
    }
    
    // 设置 Rabbit.prototype = animal 的字面意思是:“当创建了一个 new Rabbit 时,把它的 [[Prototype]] 赋值为 animal”
    Rabbit.prototype = animal;
    
    let rabbit = new Rabbit("White Rabbit"); //  rabbit.__proto__ == animal
    
    alert( rabbit.eats ); // true
    
    /**
    ------------------------     prototype        --------------------------
    Rabbit                      ----------->	animal:                                                                           eats: true
    ------------------------		      ---------------------------
                                                            ^
                                                            |
                                                            | [[Prototype]]
                                                            |
                                                ----------------------------
                                                rabbit:
                                                    name: "White Rabbit"
                                                ----------------------------
    */
    复制代码
  • 默认的F.prototype,构造器属性

    每个函数都有 "prototype" 属性,即使我们没有提供它。

    默认的 "prototype" 是一个只有属性 constructor 的对象,属性 constructor 指向函数自身

    function Rabbit() {}
    
    /* default prototype
    Rabbit.prototype = { constructor: Rabbit };
    */
    
    
     function Rabbit() {}
    // by default:
    // Rabbit.prototype = { constructor: Rabbit }
    
    let rabbit = new Rabbit(); // inherits from {constructor: Rabbit}
    
    alert(rabbit.constructor == Rabbit); // true (from prototype)
    复制代码
  • JavaScript 自身并不能确保正确的 "constructor" 函数值

    constructor存在于函数的默认 "prototype" 中,如果我们将整个默认 prototype 替换掉,那么其中就不会有 "constructor"

    为了确保正确的 "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
    // 默认的 Rabbit.prototype.constructor 被保留了下来
    
    
    // 也可以手动重新创建 constructor 属性
    Rabbit.prototype = {
      jumps: true,
      constructor: Rabbit
    };
    // 这样的 constructor 也是正确的,因为我们手动添加了它
    复制代码
修改prototype
  • 修改、删除、新增构造函数中prototype中的属性时(在这之前prototype对象没有被重新赋值),已被实例化的对象所继承的prototype属性会随着变化

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    Rabbit.prototype = { eats: false }; // (*)
    alert( rabbit.eats ); // true
    
    Rabbit.prototype.eats = false
    alert( rabbit.eats ); // true (*)对 prototype 重新赋值了
    复制代码
  • 修改构造函数的整个prototype时,已被实例化的对象所继承的prototype属性不变

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    Rabbit.prototype.eats = false
    alert( rabbit.eats ); // false
    复制代码
原型方法

__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);

alert(rabbit.eats); // true

alert(Object.getPrototypeOf(rabbit) === animal); // true

Object.setPrototypeOf(rabbit, {}); // 将 rabbit 的原型修改为 {}

// Object.create 有一个可选的第二参数:属性描述器。我们可以在此处为新对象提供额外的属性
let rabbit2 = Object.create(animal, {
  jumps: {
    value: true
  }
});
复制代码
  • Object.create 提供了一种简单的方式来浅拷贝一个对象的所有描述符(包括可枚举的和不可枚举的)`

    let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
    复制代码
  • 返回“自身”的方法,下面指的是obj对象

[[Prototype]]__proto__prototype

含义
[[Prototype]]

对象的原型,本身也是一个对象。是对象的一个特殊的隐藏属性 ,它要么为 null,要么就是对另一个对象的引用

__proto__

[[Prototype]] 的因历史原因而留下来的 getter/setter,也就是可以通过__proto__去访问[[Prototype]],是一个访问器属性。当然,推荐使用使用**Object.getPrototypeOf/Object.setPrototypeOf**去访问

prototype

为了方便理解,我将这里的prototype分为三类:分别是原生的prototype(Object.prototype)、内置对象的prototypeFunctionArrayNumber…)、普通构造函数的prototype

我通俗地理解:只要可以new的对象或者函数,都有prototype属性

  • 原生的prototype(Object.prototype)、内置对象的prototype

“一切都从对象继承而来”。所有的内建原型顶端都是 Object.prototype

image-20210906195601420

let arr = [1, 2, 3];

// 它继承自 Array.prototype
alert( arr.__proto__ === Array.prototype ); // true

// 接下来继承自 Object.prototype
alert( arr.__proto__.__proto__ === Object.prototype ); // true

// 原型链的顶端为 null
alert( arr.__proto__.__proto__.__proto__ ); // null
复制代码
  • 所有的内建对象都遵循相同的模式(pattern)
  • 方法都存储在 prototype 中(Array.prototypeObject.prototypeDate.prototype 等)
  • 对象本身只存储数据(数组元素、对象属性、日期)
  • 原始数据类型也将方法存储在包装器对象的 prototype 中:Number.prototypeString.prototypeBoolean.prototype。只有 undefinednull 没有包装器对象
  • 构造函数的prototype
  • 构造函数的原型对象,也称构造器属性。每个函数都有默认的 "prototype" 属性,是一个只有属性 constructor 的对象,属性 constructor 指向函数自身

  • 在构造函数创建实例对象时,prototype为实例对象的原型([[Prototype]])赋值。即实例对象的原型是构造函数的prototype决定的

写到这里,我就想:构造函数的原型是什么?它的原型的原型是什么?

  • Function与构造函数的prototype

    • 构造函数的__proto__ === Function.prototype

      构造函数是由Function实例化的,所有函数都是Function的对象

    • Function.__proto__ === Function.prototype

      全局的 Function 对象没有自己的属性和方法,但是,因为它本身也是一个函数,所以它也会通过原型链从自己的原型链 Function.prototype上继承一些属性和方法

function Cat(name) {
  this.name = name;
}

Cat.prototype // { constructor: Cat(name) }

// Cat 原型指向 Function 的 prototype,因为所有函数都是由 Function 构造的
Cat.__proto__ === Function.prototype; // true

// Function 的原型指向自身的 prototype,从自身的 prototype 中继承一些属性和方法
Function.__proto__ === Function.prototype; // true

复制代码
关系
  • [[Prototype]]是属于对象的,是对象的一个隐藏属性,也就是“原型”

  • __proto__[[Prototype]](“原型”)的getter/setter,就是可以通过访问对象__proto__来获取对象的原型

  • 对象是由构造函数所实例化的。prototype构造函数的默认的构造属性。在实例化对象时,为实例化对象指定原型(给对象的[[Prototype]]赋值)。当然还有Object.prototype和内置对象的prototype

function Cat(name) {
  this.name = name;
}

let cat = new Cat('Tony');
/** 
    Cat:构造函数;
    {constructor: Cat(name)}:构造函数的 prototype;
    cat:Cat 的实例化对象;

------------------------     prototype        --------------------------
Cat                         ----------->      {}:
                            <-----------	constructor: Cat(name)
------------------------     constructor      ---------------------------
                                               ^
                                               |
                                               | [[Prototype]](可通过 __proto__ 访问)
                                               |
                                              ----------------------------
                                               cat:
                                                   name: "Tony"
                                              ----------------------------
*/
// ==============================================================================


function Rabbit(name) {
  this.name = name;
}

let animal = {
  eats: true
};

// 设置 Rabbit.prototype = animal 的字面意思是:“当创建了一个 new Rabbit 时,把它的 [[Prototype]] 赋值为 animal”
Rabbit.prototype = animal;

let rabbit = new Rabbit("White Rabbit"); //  rabbit.__proto__ == animal

alert( rabbit.eats ); // true

/**
    Rabbit:构造函数;		
    animal:构造函数的 prototype;		
    rabbit:Rabbit 的实例化对象;

------------------------     prototype        --------------------------
Rabbit                      ----------->       animal:
                                                    eats: true
------------------------	              ---------------------------
                                                ^
                                                |
                                                | [[Prototype]](可通过 __proto__ 访问)
                                                |
                                              ----------------------------
                                               rabbit:
                                                   name: "White Rabbit"
                                              ----------------------------
*/
// =============================================================================

let animal = {
  eats: true
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

/**
 --------------------------------
	null
  --------------------------------
                  ^
                  | [[Prototype]]
                  |
  --------------------------------
   Object.prototype:
       toString: function
       hasOwnProperty: function
       ...
  --------------------------------
                  ^
                  | [[Prototype]]
                  |
  --------------------------------
   animal:
       eats: true
  --------------------------------
                  ^
                  | [[Prototype]]  // rabbit.__proto__ ==== animal
                  |
  --------------------------------
   rabbit:
   	jumps: true
  --------------------------------
*/
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享