乐趣区

关于前端基础:JavaScript之原型

[[Prototype]]

JavaScript 中的对象(函数也是对象)有一个非凡的 [[Prototype]] 内置属性,所谓的原型链就是由它“链”起来的。

属性查找

当援用对象的属性时会触发 [[Get]] 操作,能够了解为会执行 [[Get]](),其逻辑是先查找以后对象是否存在该属性,如果存在就应用它。否则就去递归遍历,查找[[Prototype]] 属性所援用的对象中是否存在要查找的属性,如果找到则返回,否则直到[[Prototype]]=null 时查找完结,此时返回 undefined。

在应用 for in 遍历对象时原理和查找 [[Prototype]] 链相似,任何能够通过原型链拜访到并且是 enumerable 的属性都会被枚举。应用 in 操作符来查看属性在对象中是否存在时,会查找对象的整条原型链。

属性设置和屏蔽

上面以 myObject.foo = 'bar' 为例来阐明所有可能呈现的状况:

  1. 如果 myObject 本身存在 foo 属性,不论其 [[Prototype]] 链下层是否存在,都会产生屏蔽景象,会从新赋值。
  2. 如果 myObject 本身不存在,[[Prototype]]链上也不存在,则 foo 属性会被增加到 myObject 上。
  3. 如果 myObject 本身不存在,[[Prototype]]链上存在,会呈现如下三种状况:

    1. 如果 [[Prototype]] 链上的 foo 为一般数据拜访属性,并且没有被标记为只读,那就会在 myObject 上增加 foo 属性,它是屏蔽属性。
    2. 如果 [[Prototype]] 链上的 foo 被标记为只读,如果运行在严格模式下,会抛出一个谬误。否则,这条赋值语句被疏忽,并不会产生属性屏蔽。
    3. 如果 [[Prototype]] 链上的 foo 是一个 setter,那么肯定会调用这个 setter。foo 不会增加到 myObject,也不会从新定义 foo 这个 setter。

如果心愿在第二种和第三种下也屏蔽 foo,那就不能应用 = 操作符来赋值,而是应用 Object.defineProperty() 来向 myObject 增加 foo。

上面来个例子领会一下,同时为原型继承做一个铺垫

var person = {name: 'a'};
var fn = {getName: function() {return this.name;}
};
  • 先想一下 person 为什么能够调用到 Object.prototype 中定义的办法?

Object 被定义为函数,上面会提到,只有是函数都会存在 prototype 属性,它指向一个对象,被称为原型对象,toString、hasOwnProperty 等办法就定义在该原型对象上。var person = {name: 'a'};执行时会创立一个新的对象,并且底层会先将 person 的 [[Prototype]] 属性值设置为 Object.prototype,相当于执行 Object.setPrototypeOf({name: a}, Object.prototype)。这样在整个[[Prototype]] 链上就能够找到这些办法了。

  • 再想一下咱们怎么能使 person 对象能够调用 fn 中的 getName 办法呢?
  1. 应用 Object.assign(person, fn)将 fn 的 getName 间接增加到 person 对象中。
  2. 应用 Object.setPrototypeOf(person, fn),会将 person 中的 [[Prototype]] 属性由默认的 Object 改为指向 fn 对象,而 fn 中的 [[Prototype]] 会指向 Object.prototype。在执行 person.getName()时会进行属性查找,依据下面提到的规定,在其 [[Prototype]] 链上能够找到 getName 办法,其中的 this 利用的 隐式绑定

除此之外,还有另外一种实现形式,其原理和第二条一样:

var fn = {getName: function() {return this.name;}
};

// 创立一个新对象 person,使其 [[Prototype]] 指向 fn
var person = Object.create(fn);
person.name = 'a';
person.getName(); // 'a'

函数中的 prototype

JavaScript 中的函数有一种非凡个性:所有函数默认都会领有一个名为 prototype 的私有并且不可枚举的属性,他会指向一个对象:这个对象通常被称为该函数的原型。该函数同时存在内置属性[[Prototype]],留神这两者的区别!

同时该 prototype 对象存在一个叫 constructor 的属性,会持有该函数的援用。

这些个性与上面要说的“构造函数”没有任何关系,也就是说只有是函数就有这些个性。

构造函数

JavaScript 中把首字母大写的办法称为构造函数,这只是一种约定,同时这也意味着要应用 new 关键字来调用。

应用 new 调用函数会执行上面的步骤:

  1. 创立一个全新的对象。
  2. 这个新对象会被执行 [[Prototype]] 连贯。
  3. 这个新对象会被绑定到函数调用的 this。
  4. 如果函数没有返回其它对象。那么 new 表达式中的函数调用会主动返回这个对象。

上面是伪代码

function customNew(fn) {var o = {};
  var rs = fn.apply(o, [].slice.call(arguments, 1));
  Object.setPrototypeOf(o, fn.prototype);
  return typeof rs === 'undefined' ? o : rs;
}

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

Person.prototype.getName = function() {return this.name;};

var p = customNew(Person, 'JS');
p.getName(); // 'JS'
console.log(p.constructor === Person); // true

Person 的实例能够拜访到 getNameconstructor都是基于“属性拜访”的原理。

继承

继承形式有多种,“红皮书”里也有讲到,最流行的一种是“组合继承”,即“借用构造函数”与“原型继承”组合起的一种形式。

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

Foo.prototype.myName = function() {return this.name;};

function Bar(name, label) {
  // 借用 Foo 的构造函数
  Foo.call(this, name);
  this.label = label;
}

// Bar 的原型对象被赋值为一个全新的对象,// 该对象的 [[Prototype]] 指向 Foo.prototype 对象
Bar.prototype = Object.create(Foo.prototype);

// 设置不可枚举
Object.defineProperty(Bar.prototype, 'constructor', {
  enumerable: false,
  writable: true,
  configurable: true,
  value: Bar
});

// 或者能够用 ES6 中的 setPrototypeOf 办法,它只单纯批改 Bar.prototype 中的[[Prototype]],使之指向 Foo.prototype,并不会从新赋值,所以 constructor 不会失落
// Object.setPrototypeOf(Bar.prototype, Foo.prototype);

Bar.prototype.myLabel = function() {return this.label;};

var a = new Bar("name", "label");

console.log(a.myName()); // "name"
console.log(a.myLabel()); // "label"
console.log(Bar);

如果了解了属性查找及 prototype 和[[Prototype]],再来了解 Js 中的继承就容易多了。

退出移动版