学习JavaScript"原型"

351 阅读4分钟

在上一节多次提到[[Prototype]]链,但没解释它到底是什么,那么这一节我们来详细介绍一下。

[[prototype]]

认识[[prototype]]

JavaScript中的每个对象都拥有一个原型对象。其实就是对于其他对象的引用。几乎所有的对象在创建时[[Prototype]]属性都会被赋予一个非空的值。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链。(mdn有详细的说明)

为了学习体验,我们打开控制台然后输入以下代码:

var object = {
    name:"tom"
};

object.__proto__

可以看到控制台打印了一堆属性和方法,这个我们后续说明。

思考下面的代码:

var object = {
    name:"tom"
}

var newObject = Object.create(object);

newObject.name; // "tom"

这里[[Prototype]]引用就发挥了作用,当我们试图访问newObject对象的name属性时会触发[[Get]]操作,[[Get]]操作的第一步是检查对象本身是否有这个属性,如果有的话就使用它。但如果name不在newObject中,就要使用对象的[[Prototype]]链了。

现在newObject对象的[[Prototype]]关联到了object。显然newObject.name并不存在,但是它在 object中找到了name属性。但是如果object中也找不到name并且[[Prototype]]链不为空,就会继续查找下去。

这个过程会持续到找到匹配的属性名或者查找完整条[[Prototype]]链。如果是后者的话,[[Get]]操作的返回值是undefined。(for..in遍历对象时原理和查找[[Prototype]]链类似)

[[prototype]]的尽头

从图中,可以看出来[[Prototype]]链最终都会指向内置的Object.prototype,这上面有许多我们熟悉的功能,比如toString()valueOf()hasOwnProperty(..).isPrototypeOf(..)等等。

对象属性设置

给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性值。

现在我们来学习一下这个过程:

var object = {};

object.name = "tom";
  • 如果object对象中包含名为name的数据访问属性,那么这条赋值语句只会修改已有的属性值。
  • 如果name不在object中,[[Prototype]]链就会被遍历,类似[[Get]]操作。如果原型链上找不到namename就会被直接添加到object上。
  • 如果属性名name既出现在obejct中也出现在obejct[[Prototype]]链上,那么就会发生屏蔽。obejct中包含的name属性会屏蔽原型链上的所有name属性,因此object.name总是会选择原型链中最底层的name属性。
  • 最后一种情况略微复杂我们单独说明一下,如果name仅存在于原型链上层,那么赋值语句object.name = "tom"的行为就会有些不同。

下面分析一下如果name是存在于原型链上时object.name = "tom"会出现的三种情况。

  1. 如果在[[Prototype]]链上存在名为name的数据访问属性并且没有被标记为只读(writable:false),那就会直接在obejct中添加一个名为name的新属性,它是屏蔽属性。
var object = {};

var other = Object.create(object);

other.name = "tom"; //默认other的defineProperty的writable属性为true

console.log(object.name); //tom
  1. 如果在[[Prototype]]链上存在name,但是它被标记为只读(writable:false),那么无法修改已有属性或者在obejct上创建屏蔽属性。
var object = {};

var other = Object.create(object);

other.name = "tom"; 

Object.defineProperty(other,"name",{
    writable:false
});

console.log(object.name); //undefined
  1. 如果在[[Prototype]]链上存在name并且他是一个Setter,那就一定会调用这个Settername不会被添加到object上,也不会重新定义name这个Setter
var object = {};

var other = Object.create(object);

Object.defineProperty(other,"name",{
    get:function(){
      return this._name;  
    },
    set:function(){
        this._name = "tom";
    }
});

console.log(object.name); //undefined
  • 注意属性产生的隐式屏蔽。代码如下:
var object = {
    num: 6
};

var newObject = Object.create(object);

console.log(newObject.num);  //6
console.log(object.num);  //6

console.log(newObject.hasOwnProperty("num"));  //false
console.log(object.hasOwnProperty("num"));  //true

newObject.num++;

console.log(newObject.num);  //7
console.log(object.num);  //6

这里其实也比较好说明,执行++操作相当于newObject.num=newObject.num+1。因此++运算首先会通过[[Get]]操作在[[Prototype]]查找属性num并从object.name获取当前属性值,然后给这个值加1,接着用[[Set]]将值7赋给newObject中新建的屏蔽属性name

修改委托属性时一定要小心。唯一的办法是object.num++

小结

到此简单的介绍了一下原型的基本概念。下一节将介绍原型更多的特性。

参考