本文讨论 JavaScript 中的对象创建运算 new 需要说明的是, 本文所讨论的将 new()过程分解为多个步骤, 并非一般 js 开发中的所须技巧, 而是在 js 来构建 OOP 系统的必要技术
一 JavaScript 构造器与构造过程的特点
JavaScript 中通过以下方式声明和使用构造器:
- [javascript] view plain copy print?
- function MyObject() {
- this.xxx = 1;
- }
- MyObject.prototype.yyy = 2;
- obj1 = new MyObject();
- obj2 = new MyObject();
其中 xxx 与 yyy 的不同在于: 对于 obj1 和 obj2 来说, yyy 是相同的属性的不同引用, 不同对象实例的初始值总是相同的; 而 xxx 则是各自不同的引用, 每个对象实例都不同上例中使用 12 这样的值类型数据, 并不足以体现二者的区别当我们而使用引用类型(例如数组), 就很容易看出二者的区别来了例如:
- [javascript] view plain copy print?
- function MyObject() {
- this.xxx = new Array();
- }
- MyObject.prototype.yyy = new Array();
- obj1 = new MyObject();
- obj2 = new MyObject();
- obj1.xxx.push('abc');
- obj1.yyy.push('abc');
- // 显示 1,0, 表明 obj2.xxx 并没有变化
- alert([obj1.xxx.length, obj2.xxx.length]);
- // 显示 1,1, 表明 obj2.yyy 同时变化, 与 obj1.yyy 是同一个数组
- alert([obj1.yyy.length, obj2.yyy.length]);
使用原型的另一个必要在于继承树的建立这一过程要求 new 运算符的参与, 简单说来, 就是 "子类. prototype" 必须赋值为 "new 父类()" 所产生的一个实例例如:
- [javascript] view plain copy print?
- function MyObjectEx() {
- }
- MyObjectEx.prototype = new MyObject();
- MyObjectEx.prototype.constructor = MyObjectEx;
在这个过程中, 最后面的是一行修正代码这行修正的必要性在于: new MyObject()所产生的实例 (例如 x) 的属性 x.constructor 总是指向 MyObject, 而在子类 MyObjectEx()中, 它应该是指向 MyObjectEx 的
OK 这就是全部的基础知识, 下面我们来分解这一过程
二 new()过程中的原型链维护
new 总是因为建立原型继承树而存在的, 如果没有 new 过程参与, 则当
- [javascript] view plain copy print?
- obj = new MyObjecEx()
时, 我们无法通过 instanceof 运算:
- [javascript] view plain copy print?
- obj instanceof MyObject
来了解 obj 在继承树上的关系
但是许多人并不知道, 事实上这一过程并不需要 MyObject 的参与因为 instanceof 只检查 prototype 链, 并不检查函数本身举例来说:
- [javascript] view plain copy print?
- P = {};
- X = function() {}
- Y = function() {}
- X.prototype = P;
- Y.prototype = P;
- obj = new Y();
- alert(obj instanceof X); // 显示 true
在这个示例中, obj 创建自 Y(), 但只因为 X.prototype(或者它的原型链中)指向原型 P, 所以最后显示 obj 也是 X 的一个实例可见 instanceof 的检测与 obj 是否创建自函数 X 是无关的, 并不检查函数自身
换而言之, new()在上述过程中, 只有维护原型链的作用那么, 我们事实上也可以手工来维护这个原型链这一点, 事实上也就是 ECMA Script 5th 中的 Object.create()出现的原因
Object.create(O[, Properties]) returns an object with a prototype of O and properties according to Properties (as if called through Object.defineProperties).
不考虑 Properties 的部分, 那么 Object.create 可以用以下过程来描述:
- [javascript] view plain copy print?
- Object.create = function(O) {
- function F() {};
- F.prototype = O;
- return new F();
- }
通过这个 create 方法, 我们可以创建任意的足够长的原型链, 然后把它赋值给某个构造器函数, 使原型链创建过程与构造器中的初始化过程二者分解开来例如:
- [javascript] view plain copy print?
- A = {};
- B = Object.create(A);
- C = Object.create(B);
- A.x = 1;
- B.y = 2;
- C.z = 3;
- function MyObject() {
- }
- function MyObjectEx() {
- }
- MyObject.prototype = B;
- MyObjectEx.prototype = C;
这样一来, 我们没有显式地声明 MyObject()与 MyObjectEx()之间的继承关系, 但 B 与 C 的原型关系维护了它们之间的类属关系因此:
- [javascript] view plain copy print?
- obj1 = new MyObject();
- obj2 = new MyObjectEx();
- // 显示 true, obj2 是 MyObject()的子类的实例
- alert(obj2 instanceof MyObject);
- // 显示 false, z 属性不会出现在 MyObject()的实例中
- alert('z' in obj1);
三 new()过程中的构造器调用
对于构造器 MyObject()来说, 在 new()过程中会被调用一次例如此前提到的:
- [javascript] view plain copy print?
- function MyObject() {
- this.xxx = 1;
- }
new()过程将以刚刚创建的实例为 this 来调用 MyObject(), 这使得我们有机会为这个实例 (this) 做一些初始化操作这个行为其实来自于最最早期的 JavaScript 设计在 1995 年底发布的 Netscapes Navigator 2.0 beta 以及其后的 NN2.0 正式版中, 这个最原始版本的 JavaScript 都还没有原型继承的设计, 但已经有了 new 这个运算这时所谓新对象的创建, 就是不断地为 this 赋值而已, 只不过 new 会为产生的对象维护 < obj>.constructor 属性
这件事是很容易做到的在高版本中的 Function 已经提供了 Function.call()和 Function.apply()方法, 能很方便的重现这一过程因此:
- [javascript] view plain copy print?
- obj = new MyObject(x,y,z);
就可以被分解为如下两个步骤(fixed at 2010.12.30. note: 接受 book_LoveLiness 的意见, 将 constructor 的改写过程放在原型阶段):
- [javascript] view plain copy print?
- obj = Object.create({
- constructor: MyObject
- });
- MyObject.call(obj, x, y, z);
对于上述{constructor: MyObject}, 内部隐含了如下两个过程:
- [javascript] view plain copy print?
- o = new Object();
- o.constructor = MyObject;
当用户使用自己的构造器来创建 MyObject 的原型链例如 MyObject()的父类是 ExtObject(), 而不是 Object()的时候, 就会用到这样的分解了
即使对于最早期没有 call()/apply()方法, 也没有原型继承的 JavaScript, 上述过程也可以分解为:
- [javascript] view plain copy print?
- obj = new Object();
- obj.constructor = MyObject;
- obj.constructor(a, b, c);
当然, 这一过程也就与原型继承没有了关系我们假定这就是 JavaScript 1.0 的时代吧
四构造过程分解的意义
因为原型继承 (包括原型链维护) 与构造器调用是可以分开的, 所以我们事实上也可以只使用二者之一来创建任何复杂的过程对于大型 OOP 框架来说, 是否需要维护原型链是一个深入的话题例如一些早期的 OOP 框架就不管不顾, 只考虑子类对象对父类的相似性, 而无视 instanceof 运算的效果, 这样的时代大概可以追溯到 2004 年以及之前后来 OOP 框架意识 JavaScript 的原型系统的重要性, 因此又回到正途上来,
- 通过类似于 Object.create()的方案, 来保证原型链的有效性; 又
- 通过独立的类创建或对象创建过程, 来保证系统的可扩展性
这一切, 就是我们现在的 JS OOP 的来源了
在一些具体的方法中, 有许多变形的实现在 QoBean 中的 Unique()函数综合地重现了整个过程(fixed at 2010.12.30, 考虑到无 args 参数的情况):
- [javascript] view plain copy print?
- function Unique(obj, func, args) {
- function F() {}
- F.prototype = obj;
- return func ? func.apply(new F, args||[]) : new F;
- }
对于任何一个对象 A 来说, 可以用它为原型创建一个新的实例:
- [javascript] view plain copy print?
- x = Unique(A);
或在创建之后, 使用函数 MyObject()来初始化:
- [javascript] view plain copy print?
- y = Unique(A, MyObject);
或在上述初始化中使用特定的参数:
- [javascript] view plain copy print?
- z = Unique(A, MyObject, [a, b, c]);
最后, 考虑到 reutrn valueType 的 new 的情况, 根据 Unique()给出一个新的_new 函数(添加于 2013.06.03):
- [javascript] view plain copy print?
- function _new(obj, func, args) {
- function F() {}
- F.prototype = obj, F = new F;
- obj = func ? func.apply(F, args||[]) : F;
- return (obj instanceof Object) ? obj || F : F;
- }
- function _new(obj, func, args) {
- function F() {}
- F.prototype = obj, F = new F;
- obj = func ? func.apply(F, args||[]) : F;
- return (obj instanceof Object) ? obj || F : F;
- }
最最最后, 考虑到 Object 可能被第三方代码重写导致 instanceof 判断失误的情况, 可以给出强力版本的_new(添加于 2013.06.03): [javascript] view plain copy print?
- function _new(obj, func, args) {
- function F() {}
- F.prototype = obj, F = new F;
- obj = func ? func.apply(F, args||[]) : F;
- return (typeof obj).match(/object|function/) ? obj || F : F;
- }
- function _new(obj, func, args) {
- function F() {}
- F.prototype = obj, F = new F;
- obj = func ? func.apply(F, args||[]) : F;
- return (typeof obj).match(/object|function/) ? obj || F : F;
- }
来源: https://juejin.im/entry/5aa298316fb9a028b77a69f6