现象
我们都知道,在 JavaScript
里面生成一个对象有很多种方法,其中一种便是使用构造函数。首先,定义一个构造器,在构造器内部定义对象的属性,再在构造器的原型上定义对象的方法,如下所示:
const Person = function (name) {
this.name = name;
}
Person.prototype.sayName = function () {
console.log(this.name)
}
于是,当我们对 Person
调用 new
操作符,并传入一个 name
的时候,便会生成一个新的对象, 并且该对象会继承 Person
原型上所定义的属性或方法。
然而,当我们的构造器拥有一个返回值的时候,会发生什么呢?
const Person = function (name) {
this.name = name;
return { name: 'Jason' }
}
Person.prototype.sayName = function () {
console.log(this.name)
}
const person = new Person('Tony')
person.name
// Jason
person.sayName
// undefined
person instanceof Person
// false
可以看到,当我们在构造函数中返回一个对象时,对构造函数调用 new
操作符,最后得到的将会是我们返回的对象,而当我们返回一个非对象的值的时候,得到的则是在构造函数中初始化的 this
。
注:这里的对象表示非原始值:
The ECMAScript language types are Undefined, Null, Boolean, String, Symbol, Number, and Object.
那么,造成这种现象的原因是什么呢?这个时候就需要了解当我们对一个构造函数调用 new
操作符的时候,到底发生了什么。
概念
在 ECMAScript
规范中,定义了函数对象这一概念,一个函数对象内部包含了以下几个属性(简单摘抄,完整属性列表参见前面链接):
FunctionKind ("normal", "classConstructor", "generator", "async")
ConstructorKind("base", "derived")
作为构造器的函数对象还含有内部方法[[Construct]]
。
流程
对构造函数调用 new
当我们对一个函数调用 new
操作符的时候,会执行EvaluateNew(constructExpr, arguments)
方法。
- 方法内部会对传入的参数进行一系列的校验,并通过
constructExpr
获取相应的constructor
- 当解析后得到的
constructor
不是构造器(如箭头函数)的时候,会抛出一个TypeError
- 执行语句
Return ? Construct(constructor, argList)
。 - 调用
constructor
的construct
方法 ——Return ? F.[[Construct]](argumentsList, newTarget)
Construct
- 先进行一系列的断言
- 判断
F.ConstructorKind
是否为base
(base
表示基类),如果是base
,则初始化函数内部的this
为Object.create(F.prototype)
- 判断
F.ConstructorKind
是否为base
(base
表示基类),如果是,则执行OrdinaryCallBindThis(F, calleeContext, thisArgument)
- 执行
OrdinaryCallEvaluateBody(F, argumentsList)
,得到结果result
- 如果
result
的值为一个对象,则直接返回该对象 - 如果
F.ConstructorKind
为base
,则返回上面初始化的this
- 如果
result
的值不是undefined
,则抛出一个TypeError
注:这种场景,比如在继承后的class
的constructor
中返回了一个字符串class A {} class B extends A { constructor() { return '' } } // Uncaught TypeError: Derived constructors may only return object or undefined new B()
总结
在实际的使用过程中, 我们往往很少会在构造函数中返回一个值,最常见的场景大概是 return this
以实现链式调用。在某次突发奇想,对此感到好奇并且尝试之后,一路刨根问底,才了解到简单的调用背后,包含了这么多复杂的步骤。