由于项目要用node做后台,于是最终还是入了js的坑,然后入了之后,才终于深刻的认识到,这真是一个无底深渊……
今天研究了一下js的原型机制,看的真是吐血N升,于是还是决定记录一下,方便以后查看。
首先,在js中,万物皆对象,每个对象有隐式原型(__proto__)和显示原型(prototype),隐式原型应该是不可以直接访问的,但是有些实现中允许访问修改的(node中允许),node中还提供了Object.getPrototypeOf(obj)函数来获取一个对象的隐式原型。
当访问一个对象中不存在的属性的时候,js会在这个对象的隐式原型中查找这个属性,如果还是不存在则继续在该对象隐式原型的隐式原型中查找,这样一路向上,直到找到或者找到尽头为止,而这样一条路径可以称作这个对象的原型链。
而显式原型,则是在构造函数生成对象时使用,我们在new一个对象的时候,新生成的对象的隐式原型会设为该构造函数的显式原型,且其constructor设为该构造函数显示原型的constructor,即对于:
Constructor = function () {}; obj = new Constructor();
会有
obj.__proto__ = Constructor.prototype; obj.constructor = Constructor.prototype.constructor;
然后在构造函数生成的时候,会将该构造函数的显式原型设置为一个新的对象,此新对象的constructor为该构造函数,即类似:
// this.prototype = {constructor: this}; Constructor.prototype = {constructor: Constructor};
通过这样的一个方式,保证了一个构造函数的显式原型的constructor为其自身,这样,用该构造函数生成的对象的constructor就会是该构造函数了。
那么,当我们希望实现类的继承的时候,我们可以采取:
ChildConstuctor = function () {}; ChildConstuctor.prototype = new Constructor(); childObj = new ChildConstuctor;
这样,childObj就同时可以访问到ChildConstuctor和Constructor中的属性了,但是此时,由于ChildConstuctor.prototype的constructor会为Constructor,也即是说,此时childObj.constructor为Constructor。
我们通过console.log(util.inherits.toString())可以查看到其实现如下:
function (ctor, superCtor) { ctor.super_ = superCtor; ctor.prototype = Object.create(superCtor.prototype, { constructor: { value: ctor, enumerable: false, writable: true, configurable: true } }); }
可以发现,这里采用Object.create的时候,保证了constructor为ctor,也就是上面的ChildConstuctor.prototype.constructor为ChildConstuctor本身,这样生成的对象childObj的constructor就是ChildConstuctor了,符合我们的意愿。
这里我们说一下Object.create的实现,Object.create其实就是生成一个对象,并将该对象的隐式原型设置为参数的目标,如果隐式原型不能直接设置,可以通过一个临时构造函数辅助实现:
Object.create = function (proto) { // 用正确的原型来创建一个临时构造函数 var Constructor = function () {}; Constructor.prototype = proto; // 之后用它来创建一个新的对象 return new Constructor(); }
我对于Object.create(Constructor.prototype)与new Constructor操作的区别的理解是,new操作是在Object.create执行之后再执行了Constructor本身,这样会多做一些初始化任务:
var Constructor = function () { this.attr = 'The attribute exists only when created by new.' }; var obj_by_create = Object.create(Constructor.prototype); var obj_by_new = new Constructor(); console.log(obj_by_create.attr); // undefined console.log(obj_by_new.attr); // 'The attribute exists only when created by new.'
接下来,再看一下instanceof操作的实现,instanceof其实就是判断了一个构造函数的显式原型是否在一个对象的原型链上,如果在则返回true,否则为false,我们可以按照上面的逻辑来分析一下:
-
对于childObj instanceof ChildConstuctor的判断,childObj的隐式原型是被设定为ChildConstuctor的显式原型的,那么显然满足了判断,返回true;
-
对于childObj instanceof Constuctor的判断,childObj的隐式原型是ChildConstuctor的显式原型,也即就是一个通过new或者Object.create生成的Constructor的对象,这个对象的隐式原型就是Constructor的显式原型,那么也满足了判断,返回true;
这也就说明了为什么是要在原型链上查找是否出现过了。
通过手动修改prototype,我们可以影响instanceof的判断:
var Constructor = function () {}; var obj = new Constructor(); obj instanceof Constructor; // true Constructor.prototype = {}; obj instanceof Constructor; // false var A = function () {}; var B = function () {}; var obj = new A(); obj instanceof B; // false B.prototype = A.prototype; obj instanceof B; // true
最后,我们再看一下js中各种基础的类型之间的关系,主要分4种:Number(String、Boolean等)、Function、Object、通过function语句生成的函数(如上面的Constructor)。
这些对象显式原型各不相同,但其隐式原型皆为Function的显示原型,而Function的显示原型的隐式原型为Object的显式原型,Object的显式原型的隐式原型为null,即:
Number.__proto__ = Function.__proto__ = Object.__proto__ = Constructor.__proto__ = Function.prototype; Function.prototype.__proto__ = Object.prototype; Object.prototype.__proto__ = null;
至于在实际写代码的过程中,我个人感觉而言,一个类中所有元素公用的部分,如类中各种函数、类的所有对象公用的变量可以放在构造函数的显式原型中,这样避免在各个对象中都存储一份拷贝,减少内存占用,同时可以继承给子类,而类的各个对象不同的属性,则应该在类的构造函数中设置或者使用中设置,但是这样无法通过util.inherits继承给子类。