JavaScript之原型

Posted seven

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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中的继承就容易多了。

以上是关于JavaScript之原型的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript 原型继承原型链

JavaScript之面向对象学九(原型式继承和寄生式继承)

48个值得掌握的JavaScript代码片段(上)

JavaScript之面向对象学习五(JS原生引用类型ArrayObjectString等等)的原型对象介绍

Kibana漏洞之javascript原型链污染又文件包含漏洞的非常详细的分析的黑客教程

JavaScript之面向对象学习七(动态原型模式和寄生构造函数模式创建自定义类型)