百行代码带你搞懂原型及原型链

104 阅读8分钟

写在前面

没有废话,上来就问你什么是原型
什么是原型呢?
你可以这样理解:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。
我们先从prototype属性讲起

prototype

function Rabbit() {}
// by default:
// Rabbit.prototype = { constructor: Rabbit }

每个函数都有 "prototype" 属性,即使我们没有提供它。
需要注意的是prototype是函数才会有的属性。

但是函数们都有的prototype有什么用呢?
prototype属性指向了一个对象,这个对象就是调用该构造函数创建的实例的原型。
也就是:构造函数的prototype属性指向了其实例对象的原型。

function Rabbit() {}
Rabbit.prototype = {
  eats: true
};
let rabbit = new Rabbit();
alert(rabbit.eats); // ???
Rabbit.prototype = {};
alert( rabbit.eats ); // ???

true; true
Rabbit.prototype 的赋值操作为新对象设置了 [[Prototype]],但它不影响已有的对象。

这里又引出了[[Prototype]]

[[Prototype]]

属性 [[Prototype]] 是内部的而且是隐藏的,它会引用其他对象。但它不能直接操作
设置它的方式:使用特殊的名字 __proto__

__proto__ 与 [[Prototype]] 不一样,__proto__ 是 [[Prototype]] 的因历史原因而留下来的 getter/setter

对象的[[Prototype]]关联也可以修改

// 方法一
Bar.prototype = Object.create(Foo.prototype);
// 方法二,有性能问题
Object.setPrototypeOf(Bar.prototype, Foo.prototype);

__proto__

它是每一个JavaScript对象(除了 null )都具有的一个属性,这个属性会指向该对象的原型。

.__proto__的大致实现

Object.defineProperty(Object.prototype, "__proto__", {
  get: function() {
    return Object.getPrototypeOf(this);
  },
  set: function() {
    Object.setPrototypeOf(this, o);
    return o;
  }
});

.__proto__的替代

// 创建一个以 animal 为原型的新对象
let rabbit = Object.create(animal);
//  返回对象 rabbit 的 [[Prototype]]
Object.getPrototypeOf(rabbit) === animal;
// 将 rabbit 的原型修改为 {}
Object.setPrototypeOf(rabbit, {});

.__proto__和原型的关系

大概了解了__prpto__,那么它和原型什么关系呢?

let arr = [1, 2, 3];
alert( arr.__proto__ === Array.prototype ); // ???
alert( arr.__proto__.__proto__ === Object.prototype ); // ???
alert( arr.__proto__.__proto__.__proto__ ); // ???
alert(Object.prototype.__proto__); // ???

true true null null
arr继承自 Array.prototype
接下来继承自 Object.prototype
原型链的顶端为 null
也就是:实例对象的__proto__属性指向了该对象的原型。

学习了上面这些概念,来试试下面这道题吧!

function Rabbit(name) {
  this.name = name;
}
Rabbit.prototype.sayHi = function() {
  alert(this.name);
};
let rabbit = new Rabbit("Rabbit");
rabbit.sayHi(); // ???
Rabbit.prototype.sayHi(); // ???
Object.getPrototypeOf(rabbit).sayHi(); // ???
rabbit.__proto__.sayHi(); // ???

Rabbit undefined undefined undefined
第一个调用中 this == rabbit,其他的 this 等同于 Rabbit.prototype

__proto__作为键名

let obj = {};
let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";
alert(obj[key]); // ???

[object Object],并不是 "some value"!
__proto__ 属性必须是对象或者 null。字符串不能成为 prototype。

let obj = Object.create(null);
let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";
alert(obj[key]); // ???

"some value"
通过 Object.create(null) 来创建没有原型的对象。
使用 "__proto__" 作为键是没有问题的

通过以上————实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数或者实例呢?

  • 指向实例的属性没有,因为一个构造函数可以生成多个实例
  • 指向构造函数的属性倒是有,每个原型都有一个 constructor 属性指向关联的构造函数。

constructor

constructor的指向

function Rabbit() {}
alert( Rabbit.prototype.constructor == Rabbit ); // ???
let rabbit = new Rabbit(); // inherits from {constructor: Rabbit}
alert( Rabbit.prototype.constructor === rabbit.constructor); // ???
alert(rabbit.constructor === Rabbit); // ???

true; true; true
默认的 "prototype" 是一个只有属性 constructor 的对象,属性 constructor 指向函数自身。
获取 rabbit.constructor 时,其实 rabbit 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 rabbit 的原型也就是 Rabbit.prototype 中读取,正好原型中有该属性。

function Rabbit() {}
Rabbit.prototype = {
  jumps: true
};
let rabbit = new Rabbit();
alert(rabbit.constructor === Rabbit); // ???

false
将整个默认 prototype 替换掉,那么其中就不会有 "constructor" 了

function User(name) {
  this.name = name;
}
let user = new User('John');
let user2 = new user.constructor('Pete');
alert( user2.name ); // ???

Pete
我们不触碰默认的 "prototype","constructor" 属性具有正确的值 User.prototype.constructor == User

function User(name) {
  this.name = name;
}
User.prototype = {};
let user = new User('John');
let user2 = new user.constructor('Pete');
alert( user2.name ); // ???

undefined
let user2 = new Object('Pete'),内建的 Object 构造函数会忽略参数

function Foo() {/* .. */}
Foo.prototype = {/* .. */}; // 创建一个新原型对象
var a1 = new Foo();
a1.constructor === Foo; // ???
a1.constructor === Object; // ???

false true

如何保证正确的constructor?

方法一
// 不要将 Rabbit.prototype 整个覆盖
// 可以向其中添加内容
Rabbit.prototype.jumps = true
// 默认的 Rabbit.prototype.constructor 被保留了下来

方法二
Rabbit.prototype = { jumps: true, constructor: Rabbit };
手动重新创建 constructor 属性

方法三
Object.defineProperty(Rabbit.prototype, "constructor", {
enumerable: false,
writable: true,
configurable: true,
value: Rabbit // 让.constructor指向Rabbit
})
设置原型对象的constructor

原型链

讲完上面关于原型中的三个属性,我们已经对原型有了大概的认识,下面我们介绍原型链
原型是一个对象,既然是对象,我们就可以用最原始的方式创建它

var obj = new Object();
obj.name = 'Tom';
console.log(obj.name); // ???

Tom
其实原型对象就是通过 Object 构造函数生成的,实例的 __proto__ 指向构造函数的 prototype,也就是Person.prototype的__proto__属性指向了Object.prototype
Object.prototype的原型是null。

console.log(Object.prototype.__proto__ === null) // true

null 表示“没有对象”,即该处不应该有值。
即 Object.prototype.__proto__ 的值为 null 就是 Object.prototype 没有原型。
查找属性的时候查到 Object.prototype 就可以停止查找了。

再下面就是一些综合型的知识点了。

综合

原型中的this

let user = {
    name: 'Tom',
    surname: 'Smith',
    set fullName(value) {
        [this.name, this.surname] = value.split(" ") // this是admin,不是user
    },
    get fullName() {
        return `${this.name} ${this.surname}`
    }
}
let admin = {
    __proto__: user,
    isAdmin: true
}
alert(admin.fullName) // ???
alert(user.fullName) // ???
admin.fullName = "Alice Cooper"
alert(admin.fullName) // ???
alert(user.fullName) // ???

Tom Smith; Tom Smith; Alice Cooper; Tom Smith;
this不受原型影响,始终是点符号 . 前面的对象。(谁调用的方法,谁就是this)
setter 调用 admin.fullName= 使用 admin 作为 this,而不是 user
所以admin将仅修改自己的状态,而不会修改user的状态。

    let hamster = {
      stomach: [],
      eat(food) {
        this.stomach.push(food); // *
      }
    };
    let speedy = {
      __proto__: hamster
    };
    let lazy = {
      __proto__: hamster
    };
    speedy.eat("apple");
    alert(speedy.stomach); // ???
    alert(lazy.stomach); // ???

apple; apple
this.stomach.push() 需要找到 stomach 属性,然后对其调用 push,于是顺着原型链向上查找拿到了hamster的stomach属性

    let hamster = {
      stomach: [],
      eat(food) {
        this.stomach = [food]; // *
      }
    };
    let speedy = {
      __proto__: hamster
    };
    let lazy = {
      __proto__: hamster
    };
    speedy.eat("apple");
    alert(speedy.stomach); // ???
    alert(lazy.stomach); // ???

alert <nothing>
this.stomach= 不会执行对 stomach 的查找。该值会被直接写入 this 对象

获取实例属性

alert(Object.keys(rabbit)) // jumps,name,walk

Object.keys(),仅列出可枚举的、自己的key

for (let prop in rabbit) alert(prop) // jumps name walk eats sleep 

for..in 循环会迭代继承的属性。(返回所有的key,包括继承来的)

for (let prop in rabbit) {
    let own = rabbit.hasOwnProperty(prop)
    if (own) {
        alert(`our:${prop}`) // jumps,name,walk
    } else {
        alert(prop) // eats sleep 继承来的
    }
}

内建方法obj.hasOwnProperty(key)排除继承的属性 rabbit.hasOwnProperty()来自Object,继承而来,不可枚举 同样Object.prototype的其他属性也不会被循环出来

Object.getOwnPropertyNames(obj)

Object.getOwnPropertyNames(),列出所有包括constructor

Reflect.ownKeys(obj)

返回一个由自身所有键组成的数组。

let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));

对 obj 进行真正准确地拷贝,包括所有的属性:可枚举和不可枚举的,数据属性和 setters/getters —— 包括所有内容,并带有正确的 [[Prototype]]。 Object.create 有一个可选的第二参数:属性描述器。详细

判断属性来源

var a = new Foo();
a instanceof Foo; // true

instanceof判断在a的整条[[Prototype]]链中是否有Foo.prototype指向的对象 只能处理对象a和函数(带.prototype引用的Foo)之间的关系

以下判断对象与对象之间的

function Person() { }
    Person.prototype.name = 'Tom'
    Person.prototype.age = 29
    Person.prototype.sayName = function () {
      alert(this.name + '来自原型')
    }
    var person1 = new Person()
    var person2 = new Person()
alert(Person.prototype.isPrototypeOf(person1)) // ???

true isPrototypeOf()判断有无指向关系 在person1的整条[[Prototype]]链中是否出现过Person

alert(Object.getPrototypeOf(person1) === Person.prototype) // ???

true Object.getPrototypeOf()获取一个对象的[[Prototype]]链(即获得对象的原型)

alert(person1.hasOwnProperty('name'));

true Object.hasOwnProperty('属性名')

alert("name" in person1);

true in操作符能够访问到name属性就返回true

person1.__proto__ ==== Person.prototype; // ???

true 绝大多数浏览器支持,.__proto__属性引用了内部的[[Prototype]]对象

写一个函数确定一个属性是不是原型中的属性

function hasPrototypeProperty(obj, name) {
      return (name in obj) && !obj.hasOwnProperty(name)
    }

in操作符返回true 且 hasOwnProperty()方法返回false

其他

  • 性能的角度来看,我们是从对象还是从原型链获取属性都是没区别的。它们(引擎)会记住在哪里找到的该属性,并在下一次请求中重用它。一旦有内容更改,它们就会自动更新内部缓存

  • Object.create(..)有轻微性能损失,抛弃的对象需要进行垃圾回收

  • 更改原型是一个非常缓慢的操作,因为它破坏了对象属性访问操作的内部优化。避免修改已存在的对象的 [[Prototype]]

  • 红宝书中写到 “如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性”

这样一定会发生屏蔽吗?

  1. [[Prototype]]链上层存在同名普通数据访问属性并且没有被标记为只读(writable: false)。会屏蔽,同名属性也是屏蔽属性
  2. [[Prototype]]链上层存在同名属性但是被标记为只读,那么无法修改已有属性或创建屏蔽属性,严格模式下会报错否则会忽略这条语句。不会屏蔽

只读属性会阻止[[Prototype]]下层隐式创建(屏蔽)同名属性

  1. [[Prototype]]链上层存在同名属性并且是一个setter,一定会调用这个setter,这个setter不会被重新定义。不会屏蔽

参考

书籍:《JavaScript高级程序设计》、《你不知道的JavaScript》
网站:
JavaScript深入之从原型到原型链
现代JavaScript教程

本文使用 mdnice 排版