前言
众所周知,JavaScript是使用原型模式来搭建整个面向对象系统的。而
JavaScript中实际上没有类,类是一种可选择的设计模式而不像Java那样万物皆是类。如果之前使用过一段时间Java才学JS的可能看到这一大堆new
、prototype
、instanceof…
脑子里会一片混沌。
因此,将这些与类和原型链相关的东西摆在一起统一梳理十分有必要。
提前说明,本篇除了MDN之外,还参考了《你不知道的JavaScript(上卷)》、《JavaScript设计模式与开发实践》,Vue源码。所用例子也大都出自上述来源,故本篇实则为一篇读书笔记。
类是一种设计模式。而提到类,会自然而然想到面向类的设计模式:实例化、继承和多态。
实例化
一个类就是一张蓝图。为了获得真正可以交互的对象,我们必须按照类来建造(也可以说实例化)一个东西,这个东西通常被称为实例,有需要的话,我们可以直接在实例上调用方法并访问其所有公有数据属性。
这个对象就是类中描述的所有特性的一份副本。
知识点:类通过复制操作被实例化为对象形式。
提到实例化,第一想到的是关键字new
。
“实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行[[原型]]连接。
- 这个新对象会绑定到函数调用的this。
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。”
简化的new实现过程类似于这样:
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
};
var objectFactory = function() {
var obj = new Object(), // 1. 从 Object.prototype 上克隆一个空的对象
Construstor = [].shift.call(arguments); // 取得外部传入的构造器,此例是 Person
obj.__proto__ = Construstor.prototype; // 2. 指向正确的原型
var ret = Construstor.apply(obj, arguments); // 3. 借用外部传入的构造器给 obj 设置属性
return typeof ret === 'object' ? ret : obj; // 4. 确保构造器总是会返回一个对象
};
var a = objectFactory(Person, 'sven');
console.log(a.name); // 输出:sven
console.log(a.getName()); // 输出:sven
console.log(Object.getPrototypeOf(a) === Person.prototype);
// 输出:true
——来源于《JavaScript设计模式与开发实践》
看这个具体的例子会比较好理解。
首先克隆Object.prototype作为最终可能输出的对象obj,然后获取传入的构造函数Person,接下来修改obj的__proto__
。
然后调用apply
,使用obj
为上下文,使用new
时传入的参数作为入参,执行Construstor
,后者就是传入的构造函数。
如果构造函数返回的值为对象,则传出这个对象,否则将第一行创建的obj
返回。
- _proto_
这个例子里出现的__proto__
还挺奇怪的,在MDN中有一个示例可以说明为啥会有这一步。当创建一个实例时,这个实例的__proto__
与它的类的原型相等。
function test() {}
test.prototype.myname = function () {
console.log('myname');
}
let a = new test()
console.log(a.__proto__ === test.prototype);//true
a.myname();//myname
__proto__
实现大致是这样
Object.defineProperty( Object.prototype, "__proto__", {
get: function() {
return Object.getPrototypeOf( this );
},
set: function(o) {
// ES6中的setPrototypeOf(..)
Object.setPrototypeOf( this, o );
return o;
}
} );
这里用到的__proto__
其实在MDN文档开始就再三强调了是个不推荐的概念,现在得用getPrototypeOf
和setPrototypeOf
代替对__proto__
的操作
Object.getPrototypeOf( a ) === Foo.prototype;
“new Foo()只是间接完成了我们的目标:一个关联到其他对象的新对象。”
摘录来自:“你不知道的JavaScript(上卷)。”
这个new知识点很重要,一定要记牢。
原型
1. prototype
JavaScript中的对象有一个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时[[Prototype]]属性都会被赋予一个非空的值。
所有普通的[[Prototype]]链最终都会指向内置的Object.prototype。由于所有的“普通”(内置,不是特定主机的扩展)对象都“源于”(或者说把[[Prototype]]链的顶端设置为)这个Object.prototype对象,所以它包含JavaScript中许多通用的功能。
上面说到,new
操作得到一个关联了其他对象的新对象。
继承意味着复制操作,JavaScript(默认)并不会复制对象属性。相反,JavaScript会在两个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数。
委托这个术语可以更加准确地描述JavaScript中对象的关联机制。
- constructor
function Foo() { /* .. */ }
Foo.prototype.constructor === Foo; // true
var a = new Foo();
a.constructor === Foo; // true”
函数的原型的构造器等于它自身。
Foo.prototype = { /* .. */ }; // 创建一个新原型对象
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!”
看起来是Foo()构造了a1,但实际上并不是这样的。
到底怎么回事?a1并没有.constructor
属性,所以它会委托[[Prototype]]链上的Foo.prototype
。但是这个对象也没有.constructor
属性(不过默认的Foo.prototype对象有这个属性!),所以它会继续委托,这次会委托给委托链顶端的Object.prototype
。这个对象有.constructor
属性,指向内置的Object(..)函数。
因此可以解释,当手动修改了prototype之后,若没有重新添加constructor并对其赋值,构造器constructor并不存在。
实际上,对象的.constructor
会默认指向一个函数,这个函数可以通过对象的.prototype
引用。“constructor”和“prototype”这两个词本身的含义可能适用也可能不适用。
最好的办法是记住这一点“constructor并不表示被构造”。
2. 继承
再回去看上图(原型继承),它不仅展示出对象(实例)a1到Foo.prototype的委托关系,还展示出Bar.prototype到Foo.prototype的委托关系。
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对象并关联到Foo.prototype
Bar.prototype = Object.create( Foo.prototype );
// 注意!现在没有Bar.prototype.constructor了
// 如果你需要这个属性的话可能需要手动修复一下它
Bar.prototype.myLabel = function() {
return this.label;
};
var a = new Bar( "a", "obj a" );
a.myName(); // "a"
a.myLabel(); // "obj a”
这段代码用原型风格表现了继承。
可以先回顾一下上面提到的MDN里给的那个例子,当new
一个实例后,实例a与创建它的构造函数Foo
的Foo.prototype就存在了委托关系。
Object.getPrototypeOf( a ) === Foo.prototype;
在这个继承例子里,Bar到Foo也有委托关系,是通过Object.create去实现的,那么Object.create做了什么呢?
Bar.prototype = Object.create( Foo.prototype );
- Object.create
调用
Object.create(..)
会凭空创建一个“新”对象并把新对象内部的[[Prototype]]关联到你指定的对象(本例中是Foo.prototype)。
——你不知道的JavaScript
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的
__proto__
。
——MDN
换句话说,这条语句的意思是:“创建一个新的Bar.prototype
对象并把它关联到Foo.prototype
”。
下面这两条例子接近但与Object.create
并不等同
// 和你想要的机制不一样!
Bar.prototype = Foo.prototype;
// 基本上满足你的需求,但是可能会产生一些副作用 :(
Bar.prototype = new Foo();
Bar.prototype = Foo.prototype
并不会创建一个关联到Bar.prototype
的新对象,它只是让Bar.prototype
直接引用Foo.prototype
对象。
如果对Bar.prototype
进行修改,因为引用的关系,实际上会导致Foo.prototype
发生变更。
Bar.prototype = new Foo()
的确会创建一个关联到Bar.prototype
的新对象。但是new
会使用Foo()
的构造函数调用,如果Foo()
的构造函数干了点其他的事情(在构造函数里做点什么非常正常)。
后果是,我们只是想将Bar.prototype
与Foo.prototype
关联起来,却极有可能因为new
操作干了很多额外的我们无从预料的事情。
Object.create
的polyfill如下
if(!Object.create){
Object.create=function(o){
var F=function(){};
F.prototype=o;
return new F();
};
}
Object.create(null)
Object.create(null)会创建一个拥有空(或者说null)[[Prototype]]链接的对象,这个对象无法进行委托。由于这个对象没有原型链,所以instanceof
操作符(之前解释过)无法进行判断,因此总是会返回false
。
这些特殊的空[[Prototype]]
对象通常被称作“字典”,它们完全不会受到原型链的干扰,因此非常适合用来存储数据。
这个特殊的用法值得被了解,在vue-router中可以找到它的应用如下:
export function createRouteMap (
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>
): {
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>
} {
// the path list is used to control path matching priority
const pathList: Array<string> = oldPathList || []
// $flow-disable-line
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
// $flow-disable-line
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route)
})
…………
}
存储数据
Obeject.create
唯一的缺点是需要创建一个新对象然后把旧对象抛弃掉。
这句话怎么理解呢?
因为Bar.prototype对象默认是与Object.prototype关联的,可以理解为原先是Obeject.create(Object.prototype)。使用这个方法,不是对Bar.prototype的修改,而是将原先的对象丢弃,给Bar.prototype重新赋值以新创建的Obeject.create(Foo.prototype)。这一操作可能会造成轻微的性能损失。因为抛弃的对象需要垃圾回收。
- Object.setPrototypeOf( Bar.prototype, Foo.prototype )
如果能有一个标准并且可靠的方法来修改对象的[[Prototype]]关联就好了。在ES6之前,我们只能通过设置.__proto__
属性来实现,但是这个方法并不是标准并且无法兼容所有浏览器。
ES6添加了辅助函数Object.setPrototypeOf(..)
,可以用标准并且可靠的方法来修改关联。
// ES6之前需要抛弃默认的Bar.prototype
Bar.ptototype = Object.create( Foo.prototype );
// ES6开始可以直接修改现有的Bar.prototype
Object.setPrototypeOf( Bar.prototype, Foo.prototype );
看到这里,再回去看开头用代码描述的new过程,其实有一些语句在ES6的时代都不算是最精确的。按照《你不知道的JavaScript(上卷)》与MDN里的描述,应该改成这样
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
};
var objectFactory = function() {
var obj = new Object(); // 1. 从 Object.prototype 上克隆一个空的对象
Construstor = [].shift.call(arguments); // 取得外部传入的构造器,此例是 Person
Object.setPropertypeOf(obj,Construtor.prototype); // 2. 指向正确的原型
var ret = Construstor.apply(obj, arguments); // 3. 借用外部传入的构造器给 obj 设置属性
return typeof ret === 'object' ? ret : obj; // 4. 确保构造器总是会返回一个对象
};
var a = objectFactory(Person, 'sven');
3. 检查“类”关系
假设有对象a,如何寻找对象a委托的对象?
function Foo() {
// ...
}
Foo.prototype.blah = ...;
var a = new Foo();
-
instanceof
instanceof
操作符的左操作数是一个普通的对象,右操作符是一个函数。
instanceof
回答的问题是:在a的整条[[Prototype]]链中是否有指向Foo.prototype
的对象?
a instanceof Foo; // true
有一个很常见的误解:用instanceof
去判定左边是否为右边的实例。
另:instanceof
不能直接判断两个对象是否有关联关系,所以在这个例子里实现了这样的辅助函数。
// 用来判断o1是否关联到(委托)o2的辅助函数
function isRelatedTo(o1, o2) {
function F(){}
F.prototype = o2;
return o1 instanceof F;
}
var a = {};
var b = Object.create( a );
isRelatedTo( b, a ); // true
在这个例子里,b
为Object.create( a )
的结果。我们上面提到过,这个的结果是b.prototype
会关联到a
。
显然,b
并不是调用了F
构造出来的实例,但在isRelatedTo
函数里,用instanceof
来判定这两者得到的答案是true
。
isPrototypeOf()
与instanceof
运算符不同。在表达式 "object instanceof AFunction
"中,object
的原型链是针对AFunction.prototype
进行检查的,而不是针对AFunction
本身。
—— MDN instanceof
- isPrototypeOf
isPrototypeOf()方法用于测试一个对象是否存在于另一个对象的原型链上。
Foo.prototype.isPrototypeOf( a ); // true
这个方法的好处在于我们可以直接用两个对象判定,而不是传入了函数但用来判定的却是这个函数的prototype
。
网友评论