如何安全地创建一个对象

如果我们使用构造器创建一个对象的时候,如果忘记了 new 这个关键字,可能会污染到全局。

function Foo () {
    this.name = 'foo';
}

const foo = Foo();

console.log(foo);  // undefined
console.log(name); // "foo"
1
2
3
4
5
6
7
8

那么如何避免这种情况,安全地创建对象呢?以下有两种「实用」方法。

方法 1:构造函数内部使用「严格模式」

function Foo () {
    'use strict';
    this.name = 'foo';
}

Foo();
// Uncaught TypeError: Cannot set property 'name' of undefined
1
2
3
4
5
6
7

构造函数内部使用「严格模式」,也就是第一行就要加上声明 'use strict';,然后一旦实例化对象时如果忘了 new 关键字,就会报错。

原因是 'use strict'; 命令保证了该函数在严格模式下运行。由于严格模式中,函数内部的 this 不能指向全局对象,默认等于 undefined,导致不加 new 调用会报错(JavaScript 不允许对 undefined 添加属性)。

方法 2:构造函数内部根据 this 判断

function Foo () {
    if (!this instanceof Foo) {
        return new Foo();
    }
    
    this.name = 'foo';
}

Foo();
1
2
3
4
5
6
7
8
9

构造函数内部根据 this 判断外部是否使用 new 命令,如果发现没有使用,则直接返回一个实例对象。

方法 3:构造函数内部根据 new.target 判断

function Foo () {
    if (new.target && new.target === Foo) {
        return new Foo();
    }
    
    this.name = 'foo';
}

Foo();
1
2
3
4
5
6
7
8
9

因为当构造函数由 new 关键字调用时, 函数内部的 new.target 指向当前构造函数,我们就可以根据这个特性来判断外部是否使用 new 命令。

new 一个对象的时候发生了什么

使用 new 命令实例化一个对象时,内部依次发生了如下操作:

  1. 创建了一个空对象,作为将要返回的实例对象;
  2. 将这个空对象的原型,指向构造函数的 prototype 属性;
  3. 将这个空对象赋值给构造函数内部的 this 关键字;
  4. 开始执行构造函数内部的代码。

上面的流程可以使用以下代码简洁地表示:

function _new_ (Constructor) {
    // 将 arguments 类数组对象转换成数组
    var args = [].slice.call(arguments, 0);

    // 将第一个参数(构造函数)弹出
    var Constructor = arguments.shift();

    // 创建一个空对象,并指向构造函数的 `prototype` 属性
    var context = Object.create(Constructor.prototype);
    // 等价于
    // var context = Object.setPrototypeOf({}, Constructor.prototype);
    // 也等价于
    // var context = (function () { function F () {} ; F.prototype = Constructor.prototype; return new F(); })();

    // 将这个空对象赋值给构造函数内部的 `this` 关键字,并执行构造函数内部的代码
    var result = Constructor.apply(context. args);

    // 判断构造函数内部的返回值是否为对象
    if (typeof result === 'object' && result !== null) {
        return result;
    } else {
        return context;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24