JavaScript学习笔记8--原型

  |  

前言

原型


18



[[Prototype]]

众所周知,JavaScript中的对象有一个特殊的[[Prototype]]内置属性(即__proto__),其实就是对于其它对象的引用。

早在之前我们就知道在引用对象的属性时会触发[[Get]]操作,而[[Get]]操作在对象本身没有需要的属性时,就会继续访问对象的[[Prototype]]链。这个过程会持续到找到匹配的属性名,或者整个原型链结束。

使用for…in遍历对象时原理和查找[[Prototype]]链相似,任何可以通过原型链访问到的并且是可枚举的属性都会被枚举。使用in操作符来检查属性在对象中是否存在时,同样会查找对象的整条原型链

Object.prototype

Object.prototype是所有原型链的尽头,所有的内置对象都源于Object.prototype对象。

Object.prototype包含JavaScript中许多通用的功能

属性设置和屏蔽

给一个对象设置属性,并不只是简单的添加。

如果对象已经包含了这个普通数据访问属性,这条赋值语句只会修改已有的属性值。

如果这个属性不存在于对象中,那么[[Prototype]]链就会被遍历,如果原型链上也找不到这个属性,那么这个属性将被直接赋值给对象。

但是如果原型链上存在这个属性,那么就有三种可能存在的情况:

  • 如果原型链上层存在这个普通数据访问苏还行并且未被标记为只读属性,那么就会直接在对象上添加一个新的属性,它是屏蔽属性,简单的来说它屏蔽了原型链上的属性
  • 如果在原型链上层存在这个属性,但是他被标记为只读,那么无法修改已有属性或者在对象上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。
  • 如果在原型链上存在这个属性,它是一个setter,那么就一定会调用这个setter,属性不会被添加到对象上并发生屏蔽。

对于后两者的情况,如果仍想在对象上定义一个屏蔽属性,那么就不能简单的使用等号赋值,需要使用Object.definePrototype()方法

如果需要对屏蔽方法进行委托,就不得不使用显示伪多态。通常来说,使用屏蔽得不偿失,所以应当尽量避免出现屏蔽

有些情况下会出现隐式 屏蔽

1
2
3
4
5
6
7
8
9
var anotherObject = {
a: 2
};
var myObject = Object.create(anotherObject);
anotherObject.a; // 2
myObject.a; // 2
myObject.a++; // 隐式屏蔽
anotherObject.a; // 2
myObject.a; //3

尽管myObject.a++ 看起来应该会通过委托查找并增加anotherObject.a属性,但是其实他只是通过查找,找到了原型链上的a,及anotherObject的a,然后加一,最后通过[[Put]]将值赋给myObject,所以这是一次隐式的屏蔽赋值。

“类”

JavaScript中没有类,只有对象

“类”函数

JavaScript中一直存在一种行为,就是模仿类。其本质就是利用函数的一种特殊特性:所有的函数默认都会拥有一个名为prototype的公有不可枚举属性,这个属性指向了另一个对象。

通常我们称这个对象为原型,通过function.prototype的属性来引用访问。

在JavaScript中,并不存在传统面向对象语言的复制机制。所以在JavaScript中,并不存在创建一个类的多个实例这样的操作,它只能创建多个对象,并把它们的__proto__关联到同一个对象。但是默认情况下并不会进行复制,因此这些对象并不会完全失去联系,他们仍是互相关联的。

本质上来说,所谓的原型继承,其实就是一个对象(new Function()形式创建的对象)可以通过委托的形式访问另一个对象(及Function.prototype指向的对象)的属性和函数。

“构造函数”

那么到底是因为什么原因让我们觉得JavaScript中存在”类”呢。其实就是因为new关键字。这使得我们在调用函数时,看起来像是执行了类的构造函数方法。

Function.prototype具有一个公有的且不可枚举的属性constructor,这个属性引用的是对象的关联函数,指向创建这个对象的函数

1
2
3
4
5
function Foo() {
}
Foo.prototype.constructor === Foo // true
var a = new Foo();
a.constructor === Foo; // true

从这个例子上来看,a也具有constructor属性。但是a本身并没有constructor属性,并且虽然a.constructor指向Foo函数,但是这个属性并不表示a就是由Foo构造的。

实际上a对于constructor的引用同样被委托给了Foo.prototype,而Foo.prototype.constructor默认指向Foo。

这和构造毫无关系。如果我们创建一个新对象并替换了函数默认的prototype对象的引用,那么新对象并不会自动获得constructor属性。

实际上,构造函数与JavaScript中其它普通函数没有任何区别,是因为new关键字的调用,才将这个函数变成了所谓的构造函数。实际上,new会劫持所有普通函数并用构造对象的形式来调用它

继承

典型的原型风格的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Foo(name) {
this.name = name
}
Foo.prototype.myName = function() {
return this.name
}
function Bar(name, label) {
Foo.call(this, name);
this.label = label;
}
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.myLabel = function() {
return this.label;
}
let a = new Bar("a", "obj a");
a.myName(); //"a"
a.myLabel(); // "obj a"

这段代码的核心在于Bar.prototype = Object.create(Foo.protoype)。调用Object.create会创建一个新的对象。这个语句的实际功效就是创建一个新的Bar.prototype对象并把它关联到Foo.prototype。

需要注意的是,当我们关联原型时,需要创建新的对象,而不是使用原始的关联对象

常见的错误的关联方式有

1
2
Bar.prototype = Foo.prototype;
Bar.prototype = new Foo();

这两种方式都有一些问题。

首先,第一中本质上并不会创建一个关联的新对象,准确的来说他只是让Bar.prototype直接引用了Foo.prototype对象。所以当我们修改Bar.prototype时,Foo的prototype也会跟着改变。

对于第二种,的确会创建一个关联到Bar.prototype的新对象。但是它使用了Foo的构造函数调用,当Foo有一些副作用的时候(比如写日志、修改状态、注册其它对象)虽然这不会对Bar造成影响,但是对于要继承Bar的后代来说,后果将十分严重。

因此,要创建一个合适的关联对象,我们必须使用Object.create(),虽然这样需要创建一个新对象然后将旧的原型对象抛弃,不能直接修改已有的默认对象

ES6引入了Object.setPrototypeOf()函数,可以用标准并且可靠的方法来修改关联

1
2
Bar.prototype = Object.create(Foo.prototype); //将抛弃原有的默认的Bar.prototype
Object.setPrototypeOf(Bar.prototype, Foo.prototype); //直接修改原有的Bar.prototype

如果忽略对原有对象的抛弃导致的垃圾回收带来的性能损耗,使用Object.create()其实更为简便

检查类关系

如何找寻一个对象的委托对象,或者说如何找到一个对象是继承自哪个对象的。

检查一个实例的继承祖先通常被成为内省(或反射)

站在类的角度来判断,我们可以使用instanceof操作符。a instanceof Foo,及判断Foo函数是否存在与a的原型链中。

但是使用内置的bind()函数来生成一个硬绑定函数的话,该函数就不存在prototype属性。在这样的函数上使用instanceof操作符的话,目标函数的prototype会代替硬绑定函数的prototype

还有一种方法就是使用对象的自有方法isPrototypeOF()

1
Foo.prototype.isPrototypeOf(a)

ifPrototypeOf()的作用在于判断a的原型链中是否出现过Foo.prototype这个对象

这个方法并不需要使用函数,他直接在两个对象之间判断它们的关联关系

除此之外我们也可以直接过的一个对象的原型链Object.getPrototypeOf(a),除此之外大多数浏览器也支持一种非标准的访问方法a.__proto____proto__属性可以引用内部的原型对象,甚至可以通过套接引用的方式遍历一个对象完整的原型链。但是__proto__并不是正在使用的对象的属性,他和一些内置函数一样都是Object.prototype的属性。

只有在一些特殊的情况下需要设置函数的默认prototype对象的原型,让他引用其它对象。这样可以避免使用全新的对象替换默认的原型对象。此外,为了提高代码的可读性,最好将原型对象关联看作是只读的。

对象关联

原型机制就是存在于对象中的一个内部链接,他会引用其它对象。当在对象上没有找到需要的属性或者方法引用时,JS引擎就会继续在原型关联的对象上进行查找,知道遍历整个原型链

创建关联

Object.create会创建要给新对象,并且将新对象关联到它的参数对象上,通过它我们不再需要通过类的形式来创建两个对象之间的关联

文章目录
  1. 1. [[Prototype]]
    1. 1.1. Object.prototype
    2. 1.2. 属性设置和屏蔽
  2. 2. “类”
    1. 2.1. “类”函数
    2. 2.2. “构造函数”
    3. 2.3. 继承
  3. 3. 对象关联
    1. 3.1. 创建关联