你真的弄明白new了吗

12bet,好久没有写点东西了,总觉得自己应该写点牛逼的,却又不知道如何下笔。既然如此,还是回归最基本的吧,今天就来说一说这个new。关于javascript的new关键字的内容上网搜一搜还真不少,12bet,大家都说new干了3件事:

  • 创建一个空对象
  • 将空对象的__proto__指向构造函数的prototype
  • 使用空对象作为上下文调用构造函数

文字比较难懂,翻译成javascript:

function Base() {  
    this.str = "aa";
}
// new Base()干了下面的事
var obj  = {};  
obj.__proto__ = Base.prototype;  
Base.call(obj);  

想想是这么回事哈,那就赶快试试12博体育,:

var b = new Base();  
console.dir(b); // Base {str: 'aa', __proto__: Base}  

12bet,好像是正确的,但是真的正确吗???

真的就3件事?

12bet,每个对象都有一个constructor属性,那么我们来试试看new出来的实例的constructor是什么吧。

console.dir(b.constructor); // [Function: Base]  

可以看出实例b的constructor属性就是Base,那么我们可以猜测new是不是至少还做了第4件事:

b.constructor = Base;  

以上结果看似正确,下面我们进行一点修改,这里我们修改掉原型的constructor属性:

Base.prototype.constructor = function Other(){ };  
var b = new Base();  
console.dir(b.constructor); // [Function: Other]  

情况就不一样了,可以看出,之前的猜测是错误的,第4件事应该是这样的:

b.constructor = Base.prototype.constructor;  

这里犯了一个错误,那就是没有理解好这个constructor的实质:当我们创建一个函数时,会自动生成对应的原型,这个原型包含一个constructor属性,使用new构造的实例,可以通过原型链查找到constructor。如下图所示:

constructor

如果构造函数有返回值呢?

一般情况下构造函数没有返回值,但是我们依旧可以得到该对象的实例;如果构造函数有返回值,凭直觉来说情况应该会不一样。我们对于之前的构造函数进行一点点修改:

function Base() {  
    this.str = "aa";
    return 1;
    // return "a";
    // return true;
}
var b = new Base();  
console.dir(b); // { str: 'aa'}  

我们在构造函数里设置的返回值好像没什么用,12博体育,返回的还是原来对象的实例,换一些例子试试:

function Base() {  
    this.str = "aa";
    return [1];
    // return {a:1};
}
var b = new Base();  
console.dir(b); // [1] or {a: 1}  

此时结果就不一样了,从上面的例子可以看出,如果构造函数返回的是原始值,那么这个返回值会被忽略,如果返回的是对象,就会覆盖构造的实例

new至少做了4件事

总结一下,new至少做了4件事:

// new Base();
// 1.创建一个空对象 obj
var obj = {};  
// 2.设置obj的__proto__为原型
obj.__proto__ = Base.prototype;  
// 3.使用obj作为上下文调用Base函数
var ret = Base.call(obj);  
// 4.如果构造函数返回的是原始值,那么这个返回值会被忽略,如果返回的是对象,就会覆盖构造的实例
if(typeof ret == 'object'){  
    return ret;
} else {
    return obj;
}

new的不足

在《Javascript语言精粹》(Javascript: The Good Parts)中,道格拉斯认为应该避免使用new关键字:

If you forget to include the new prefix when calling a constructor function, then this will not be bound to the new object. Sadly, this will be bound to the global object, so instead of augmenting your new object, you will be clobbering global variables. That is really bad. There is no compile warning, and there is no runtime warning.

大意是说在应该使用new的时候如果忘了new关键字,会引发一些问题。最重要的问题就是影响了原型查找,原型查找是沿着__proto__进行的,而任何函数都是Function的实例,一旦没用使用new,你就会发现什么属性都查找不到了,因为相当于直接短路了。如下面例子所示,没有使用new来创建对象的话,就无法找到原型上的fa1属性了:

function F(){ }  
F.prototype.fa1 = "fa1";
console.log(F.fa1);       // undefined  
console.log(new F().fa1); // fa1  

这里我配合一张图来说明其中原理,黄色的线为原型链,使用new构造的对象可以正常查找到属性fa1,没有使用new则完全走向了另外一条查找路径:

原型查找

以上的问题对于有继承的情况表现得更为明显,沿着原型链的方法和属性全都找不到,你能使用的只有短路之后的Function.prototype的属性和方法了。

当然了,遗忘使用任何关键字都会引起一系列的问题。再退一步说,这个问题是完全可以避免的:

function foo()  
{   
   // 如果忘了使用关键字,这一步骤会悄悄帮你修复这个问题
   if ( !(this instanceof foo) )
      return new foo();
   // 构造函数的逻辑继续……
}

可以看出new并不是一个很好的实践,道格拉斯将这个问题描述为:

This indirection was intended to make the language seem more familiar to classically trained programmers, but failed to do that, as we can see from the very low opinion Java programmers have of JavaScript. JavaScript’s constructor pattern did not appeal to the classical crowd. It also obscured JavaScript’s true prototypal nature. As a result, there are very few programmers who know how to use the language effectively.

简单来说,JavaScript是一种prototypical类型语言,在创建之初,是为了迎合市场的需要,让人们觉得它和Java是类似的,才引入了new关键字。Javascript本应通过它的Prototypical特性来实现实例化和继承,但new关键字让它变得不伦不类。

再说一点关于constructor的

虽然使用new创建新对象的时候讨论了这个constructor属性,但是这个属性似乎并没有什么用,也许设置这个属性就是一种习惯,能够让其他人直观理解对象之间的关系。

参考

  • 再谈javascript面向对象编程
  • JavaScript的实例化与继承:请停止使用new关键字
Author image
关于 superlin
Beijing, CN 主页
The reason why a great man is great is that he resolves to be a great man.
默认颜色 边栏居左 边栏居右