JS 中的 new
直接使用 {}
花括号可以很方便地创建一个对象,但是当我们想要创建很多对象时,如果还采用直接使用 {}
的方式就需要写很多冗余代码。JavaScript 提供了 new
关键字,我们可以对构造函数使用 new
操作符来创建一类相似的对象。
构造函数
构造函数在技术上是常规函数。不过有两个约定:
- 它们的命名通常以大写字母开头(语法上并未限制,但这是普遍的共识、约定)。
- 它们只能由
new
操作符来执行(如果直接调用,这时它就不是构造函数了,而是常规函数)。
function User(name) {
this.name = name;
this.isAdmin = false;
}
const user = new User("Jack");
console.log(user.name); // Jack
console.log(user.isAdmin); // false
2
3
4
5
6
7
8
9
当一个函数被使用 new
操作符执行时,它会经历以下步骤:
- 一个新的空对象被创建并赋值给
this
。 - 函数体执行。通常它会修改
this
对象,比如为其添加新的属性。 - 返回
this
对象。
换句话说,执行 new User(...)
时,做的就是类似下面的事情:
function User(name) {
// this = {};(隐式创建)
// 添加属性到 this
this.name = name;
this.isAdmin = false;
// return this;(隐式返回)
}
2
3
4
5
6
7
8
9
故 const user = new User("Jack")
可等价为以下代码:
const user = {
name: "Jack",
isAdmin: false
};
2
3
4
现在,如果我们想创建其他用户,我们可以调用 new User("Ann")
、new User("Alice")
等。代码量比每次都使用 {}
字面量的方式去创建要少,而且更易阅读。
这就是构造器的主要目的 —— 实现可重用的对象创建代码。
提示
从技术上讲,任何函数(除了箭头函数,它没有自己的 this
)都可以用作构造器。即可以通过 new
来运行。“首字母大写”是一个共同的约定,以明确表示一个函数将被使用 new
来运行。
new.target
在一个函数内部,我们可以使用 new.target
属性来检查它是否被使用 new
关键字进行调用了。
常规调用时,它为 undefined
。使用 new
调用时,则等于该函数:
function User() {
console.log(new.target);
console.log(new.target === User);
}
// 直接调用(不使用 `new` 关键字):
User(); // undefined, false
// 使用 `new` 关键字调用
new User(); // function User { ... }, true
2
3
4
5
6
7
8
9
10
我们也可以让常规调用和使用 new
关键字调用做相同的工作,像这样:
function User(name) {
if (!new.target) {
return new User(name);
}
this.name = name;
}
const john = User("John");
console.log(john.name);
2
3
4
5
6
7
8
9
10
这种方法有时被用在库中以使语法更加灵活。这样其他人在调用函数时,无论是否使用了 new
,程序都能如期工作。
不过,到处都使用它并不是一件好事,因为省略了 new
使得很难直观地知道代码在干啥。而通过使用 new
关键字,我们都知道代码正在创建一个新对象。
构造函数的 return
通常,构造器函数内没有 return
语句。它们的任务是将所有必要的东西写入 this
,并自动返回 this
。
但是,如果构造器函数内有 return
语句,那么:
- 如果
return
返回的是一个对象(不含null
),则返回这个对象,而不是this
。 - 如果
return
返回的是一个原始类型(包括null
),则忽略return
语句,继续返回this
。
换句话说,带有对象的 return
语句返回该对象,其他情况下都返回 this
。
例如,这里 return
通过返回一个对象覆盖了 this
:
function BigUser() {
this.name = "小明";
return { name: "小王" };
}
console.log(new BigUser().name); // 小王
2
3
4
5
6
这里有一个 return
为 undefined
的例子(或者我们可以在它之后放置一个原始类型,结果是一样的):
function SmallUser() {
this.name = "小小王";
return;
}
console.log(new SmallUser().name); // 小小王
2
3
4
5
6
通常构造器函数里都是没有 return
语句的,这里只做了解即可。
省略括号
顺便说一下,如果没有参数,我们可以省略 new
后的括号:
const user = new User;
// 等同于
const user = new User();
2
3
这里省略括号不是一种好风格,但是语法规范上是允许的。
构造器中的方法
使用构造函数来创建对象有很大的灵活性。构造函数可能有一些函数入参,这些参数定义了如何构造对象。
当然,我们不仅可以在 this
上添加属性,还可以添加方法。
function User(name) {
this.name = name;
this.sayHi = function () {
console.log(`我的名字是: ${this.name}`);
};
}
const user = new User("李白");
user.sayHi(); // 我的名字是李白
2
3
4
5
6
7
8
9
10
11
手写一个 new
问题描述
由于 new
是 JavaScript 中的关键字,我们不可能另外实现一个自己的关键字。所以这里要求我们实现一个函数 myNew
,要求:myNew(Person, '李白')
等价于 new Person('李白')
。
具体实现
function myNew() {
// 1、创建一个空对象
const obj = {}
// 获取构造方法(并将其从 `arguments` 中移出)
const constructor = [].shift.call(arguments)
// 2、将新对象的原型指向构造方法的 `prototype` 对象上
obj.__proto__ = constructor.prototype
/**
* 3、获取到构造方法的返回值
* 如果原先构造方法有返回值,且是对象,
* 那么原始的 `new` 会把这个对象返回出去
* (基本类型会忽略)
*
* 原始 `arguments` 的第一个参数已经在前面被 `shift` 了,
* 所以剩下的参数全都是构造方法需要的值
*/
const ret = constructor.apply(obj, arguments)
// `(ret || obj)` 是为了判断 `null`,当为 `null` 时,也返回新对象
return typeof ret === 'object' ? (ret || obj) : obj
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
使用
function Person(name, age) {
this.name = name
this.age = age
}
const p = myNew(Person, 'cheny', 28)
// true
console.log(p instanceof Person);
2
3
4
5
6
7
8