【JavaScript 从入门到精通】构造器|可选链
@[toc]
构造器和操作符 “new”
常规的 {...}
语法允许创建一个对象。但是我们经常需要创建很多类似的对象,例如多个用户或菜单项等。
这可以使用构造函数和 "new"
操作符来实现。
构造函数
构造函数在技术上是常规函数。不过有两个约定:
- 它们的命名以大写字母开头。
- 它们只能由
"new"
操作符来执行。
例如:
function User(name) {
this.name = name;
this.isAdmin = false;
}
let user = new User("Jack");
alert(user.name); // Jack
alert(user.isAdmin); // false
当一个函数被使用 new
操作符执行时,它按照以下步骤:
- 一个新的空对象被创建并分配给
this
。 - 函数体执行。通常它会修改
this
,为其添加新的属性。 - 返回
this
的值。
换句话说,new User(...)
做的就是类似的事情:
function User(name) {
// this = {};(隐式创建)
// 添加属性到 this
this.name = name;
this.isAdmin = false;
// return this;(隐式返回)
}
所以 new User("Jack")
的结果是相同的对象:
let user = {
name: "Jack",
isAdmin: false
};
现在,如果我们想创建其他用户,我们可以调用 new User("Ann")
,new User("Alice")
等。比每次都使用字面量创建要短得多,而且更易于阅读。
这是构造器的主要目的 —— 实现可重用的对象创建代码。
让我们再强调一遍 —— 从技术上讲,任何函数(除了箭头函数,它没有自己的 this
)都可以用作构造器。即可以通过 new
来运行,它会执行上面的算法。“首字母大写”是一个共同的约定,以明确表示一个函数将被使用 new
来运行。
ℹ️new function() { … }
如果我们有许多行用于创建单个复杂对象的代码,我们可以将它们封装在一个立即调用的构造函数中,像这样:
// 创建一个函数并立即使用 new 调用它 let user = new function() { this.name = "John"; this.isAdmin = false; // ……用于用户创建的其他代码 // 也许是复杂的逻辑和语句 // 局部变量等 };
这个构造函数不能被再次调用,因为它不保存在任何地方,只是被创建和调用。因此,这个技巧旨在封装构建单个对象的代码,而无需将来重用。
构造器模式测试:new.target
ℹ️进阶内容
本节涉及的语法内容很少使用,除非你想了解所有内容,否则你可以直接跳过该语法。
在一个函数内部,我们可以使用 new.target
属性来检查它是否被使用 new
进行调用了。
对于常规调用,它为 undefined,对于使用 new
的调用,则等于该函数:
function User() {
alert(new.target);
}
// 不带 "new":
User(); // undefined
// 带 "new":
new User(); // function User { ... }
它可以被用在函数内部,来判断该函数是被通过 new
调用的“构造器模式”,还是没被通过 new
调用的“常规模式”。
我们也可以让 new
调用和常规调用做相同的工作,像这样:
function User(name) {
if (!new.target) { // 如果你没有通过 new 运行我
return new User(name); // ……我会给你添加 new
}
this.name = name;
}
let john = User("John"); // 将调用重定向到新用户
alert(john.name); // John
这种方法有时被用在库中以使语法更加灵活。这样人们在调用函数时,无论是否使用了 new
,程序都能工作。
不过,到处都使用它并不是一件好事,因为省略了 new
使得很难观察到代码中正在发生什么。而通过 new
我们都可以知道这创建了一个新对象。
构造器的 return
通常,构造器没有 return
语句。它们的任务是将所有必要的东西写入 this
,并自动转换为结果。
但是,如果这有一个 return
语句,那么规则就简单了:
- 如果
return
返回的是一个对象,则返回这个对象,而不是this
。 - 如果
return
返回的是一个原始类型,则忽略。
换句话说,带有对象的 return
返回该对象,在所有其他情况下返回 this
。
例如,这里 return
通过返回一个对象覆盖 this
:
function BigUser() {
this.name = "John";
return { name: "Godzilla" }; // <-- 返回这个对象
}
alert( new BigUser().name ); // Godzilla,得到了那个对象
这里有一个 return
为空的例子(或者我们可以在它之后放置一个原始类型,没有什么影响):
function SmallUser() {
this.name = "John";
return; // <-- 返回 this
}
alert( new SmallUser().name ); // John
通常构造器没有 return
语句。这里我们主要为了完整性而提及返回对象的特殊行为。
ℹ️省略括号
顺便说一下,如果没有参数,我们可以省略
new
后的括号:let user = new User; // <-- 没有参数 // 等同于 let user = new User();
这里省略括号不被认为是一种“好风格”,但是规范允许使用该语法。
构造器中的方法
使用构造函数来创建对象会带来很大的灵活性。构造函数可能有一些参数,这些参数定义了如何构造对象以及要放入什么。
当然,我们不仅可以将属性添加到 this
中,还可以添加方法。
例如,下面的 new User(name)
用给定的 name
和方法 sayHi
创建了一个对象:
function User(name) {
this.name = name;
this.sayHi = function() {
alert( "My name is: " + this.name );
};
}
let john = new User("John");
john.sayHi(); // My name is: John
/*
john = {
name: "John",
sayHi: function() { ... }
}
*/
[类](类(编程术语)_百度百科 (baidu.com)) 是用于创建复杂对象的一个更高级的语法,我们稍后会讲到。
总结
- 构造函数,或简称构造器,就是常规函数,但大家对于构造器有个共同的约定,就是其命名首字母要大写。
- 构造函数只能使用
new
来调用。这样的调用意味着在开始时创建了空的this
,并在最后返回填充了值的this
。
我们可以使用构造函数来创建多个类似的对象。
JavaScript 为许多内建的对象提供了构造函数:比如日期 Date
、集合 Set
以及其他我们计划学习的内容。
ℹ️对象,我们还会回来哒!
在本章中,我们只介绍了关于对象和构造器的基础知识。它们对于我们在下一章中,学习更多关于数据类型和函数的相关知识非常重要。
在我们学习了那些之后,我们将回到对象,在 【原型,继承】 和 【类】章节中深入介绍它们。
✅任务
两个函数 — 一个对象
重要程度:two:
是否可以创建像 new A() == new B()
这样的函数 A
和 B
?
function A() { ... }
function B() { ... }
let a = new A;
let b = new B;
alert( a == b ); // true
如果可以,请提供一个它们的代码示例。
解决方案
是的,这是可以的。
如果一个函数返回一个对象,那么
new
返回那个对象而不是this
。所以它们可以,例如,返回相同的外部定义的对象
obj
:let obj = {}; function A() { return obj; } function B() { return obj; } alert( new A() == new B() ); // true
创建 new Calculator
重要程度:five:
创建一个构造函数 Calculator
,它创建的对象中有三个方法:
read()
使用prompt
请求两个值并把它们记录在对象的属性中。sum()
返回这些属性的总和。mul()
返回这些属性的乘积。
例如:
let calculator = new Calculator();
calculator.read();
alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );
解决方案
function Calculator() { this.read = function() { this.a = +prompt('a?', 0); this.b = +prompt('b?', 0); }; this.sum = function() { return this.a + this.b; }; this.mul = function() { return this.a * this.b; }; } let calculator = new Calculator(); calculator.read(); alert( "Sum=" + calculator.sum() ); alert( "Mul=" + calculator.mul() );
创建 new Accumulator
重要程度: 5
创建一个构造函数 Accumulator(startingValue)
。
它创建的对象应该:
- 将“当前 value”存储在属性
value
中。起始值被设置到构造器startingValue
的参数。 read()
方法应该使用prompt
来读取一个新的数字,并将其添加到value
中。
换句话说,value
属性是所有用户输入值与初始值 startingValue
的总和。
下面是示例代码:
let accumulator = new Accumulator(1); // 初始值 1
accumulator.read(); // 添加用户输入的 value
accumulator.read(); // 添加用户输入的 value
alert(accumulator.value); // 显示这些值的总和
解决方案
function Accumulator(startingValue) { this.value = startingValue; this.read = function() { this.value += +prompt('How much to add?', 0); }; } let accumulator = new Accumulator(1); accumulator.read(); accumulator.read(); alert(accumulator.value);
可选链 “?.”
⚠️最近新增的特性
这是一个最近添加到 JavaScript 的特性。 旧式浏览器可能需要 polyfills.
可选链 ?.
是一种访问嵌套对象属性的安全的方式。即使中间的属性不存在,也不会出现错误。
“不存在的属性”的问题
如果你才刚开始读此教程并学习 JavaScript,那可能还没接触到这个问题,但它却相当常见。
举个例子,假设我们有很多个 user
对象,其中存储了我们的用户数据。
我们大多数用户的地址都存储在 user.address
中,街道地址存储在 user.address.street
中,但有些用户没有提供这些信息。
在这种情况下,当我们尝试获取 user.address.street
,而该用户恰好没提供地址信息,我们则会收到一个错误:
let user = {}; // 一个没有 "address" 属性的 user 对象
alert(user.address.street); // Error!
这是预期的结果。JavaScript 的工作原理就是这样的。因为 user.address
为 undefined
,尝试读取 user.address.street
会失败,并收到一个错误。
但是在很多实际场景中,我们更希望得到的是 undefined
(表示没有 street
属性)而不是一个错误。
……还有另一个例子。在 Web 开发中,我们可以使用特殊的方法调用(例如 document.querySelector('.elem')
)以对象的形式获取一个网页元素,如果没有这种对象,则返回 null
。
// 如果 document.querySelector('.elem') 的结果为 null,则这里不存在这个元素
let html = document.querySelector('.elem').innerHTML; // 如果 document.querySelector('.elem') 的结果为 null,则会出现错误
同样,如果该元素不存在,则访问 null
的 .innerHTML
属性时会报错。在某些情况下,当元素的缺失是没问题的时候,我们希望避免出现这种错误,而是接受 html = null
作为结果。
我们如何实现这一点呢?
可能最先想到的方案是在访问该值的属性之前,使用 if
或条件运算符 ?
对该值进行检查,像这样:
let user = {};
alert(user.address ? user.address.street : undefined);
这样可以,这里就不会出现错误了……但是不够优雅。就像你所看到的,"user.address"
在代码中出现了两次。
我们看一个以相同方式获取 document.querySelector
的例子:
let html = document.querySelector('.elem') ? document.querySelector('.elem').innerHTML : null;
我们可以看到用于进行元素搜索的 document.querySelector('.elem')
在这里实际上被调用了两次。这样不优雅。
对于嵌套层次更深的属性,代码会变得更丑,因为需要更多的重复。
例如,让我们以相同的方式尝试获取 user.address.street.name
。
let user = {}; // user 没有 address 属性
alert(user.address ? user.address.street ? user.address.street.name : null : null);
这样就太扯淡了,并且这可能导致写出来的代码很难让别人理解。
这里有一种更好的实现方式,就是使用 &&
运算符:
let user = {}; // user 没有 address 属性
alert( user.address && user.address.street && user.address.street.name ); // undefined(不报错)
依次对整条路径上的属性使用与运算进行判断,以确保所有节点是存在的(如果不存在,则停止计算),但仍然不够优雅。
就像你所看到的,在代码中我们仍然重复写了好几遍对象属性名。例如在上面的代码中,user.address
被重复写了三遍。
这就是为什么可选链 ?.
被加入到了 JavaScript 这门编程语言中。那就是彻底地解决以上所有问题!
可选链
如果可选链 ?.
前面的值为 undefined
或者 null
,它会停止运算并返回 undefined
。
为了简明起见,在本文接下来的内容中,我们会说如果一个属性既不是 null
也不是 undefined
,那么它就“存在”。
换句话说,例如 value?.prop
:
- 如果
value
存在,则结果与value.prop
相同, - 否则(当
value
为undefined/null
时)则返回undefined
。
下面这是一种使用 ?.
安全地访问 user.address.street
的方式:
let user = {}; // user 没有 address 属性
alert( user?.address?.street ); // undefined(不报错)
代码简洁明了,也不用重复写好几遍属性名。
这里是一个结合 document.querySelector
使用的示例:
let html = document.querySelector('.elem')?.innerHTML; // 如果没有符合的元素,则为 undefined
即使 对象 user
不存在,使用 user?.address
来读取地址也没问题:
let user = null;
alert( user?.address ); // undefined
alert( user?.address.street ); // undefined
请注意:?.
语法使其前面的值成为可选值,但不会对其后面的起作用。
例如,在 user?.address.street.name
中,?.
允许 user
为 null/undefined
(在这种情况下会返回 undefined
)也不会报错,但这仅对于 user
。更深层次的属性是通过常规方式访问的。如果我们希望它们中的一些也是可选的,那么我们需要使用更多的 ?.
来替换 .
。
⚠️不要过度使用可选链
我们应该只将
?.
使用在一些东西可以不存在的地方。例如,如果根据我们的代码逻辑,
user
对象必须存在,但address
是可选的,那么我们应该这样写user.address?.street
,而不是这样user?.address?.street
。那么,如果
user
恰巧为 undefined,我们会看到一个编程错误并修复它。否则,如果我们滥用?.
,会导致代码中的错误在不应该被消除的地方消除了,这会导致调试更加困难。
⚠️**
?.
前的变量必须已声明**如果未声明变量
user
,那么user?.anything
会触发一个错误:// ReferenceError: user is not defined user?.address;
?.
前的变量必须已声明(例如let/const/var user
或作为一个函数参数)。可选链仅适用于已声明的变量。
短路效应
正如前面所说的,如果 ?.
左边部分不存在,就会立即停止运算(“短路效应”)。
因此,如果在 ?.
的右侧有任何进一步的函数调用或操作,它们均不会执行。
例如:
let user = null;
let x = 0;
user?.sayHi(x++); // 没有 "user",因此代码执行没有到达 sayHi 调用和 x++
alert(x); // 0,值没有增加
其它变体:?.(),?.[]
可选链 ?.
不是一个运算符,而是一个特殊的语法结构。它还可以与函数和方括号一起使用。
例如,将 ?.()
用于调用一个可能不存在的函数。
在下面这段代码中,有些用户具有 admin
方法,而有些没有:
let userAdmin = {
admin() {
alert("I am admin");
}
};
let userGuest = {};
userAdmin.admin?.(); // I am admin
userGuest.admin?.(); // 啥都没发生(没有这样的方法)
在这两行代码中,我们首先使用点符号(userAdmin.admin
)来获取 admin
属性,因为我们假定对象 userAdmain
存在,因此可以安全地读取它。
然后 ?.()
会检查它左边的部分:如果 admin
函数存在,那么就调用运行它(对于 userAdmin
)。否则(对于 userGuest
)运算停止,没有报错。
如果我们想使用方括号 []
而不是点符号 .
来访问属性,语法 ?.[]
也可以使用。跟前面的例子类似,它允许从一个可能不存在的对象上安全地读取属性。
let key = "firstName";
let user1 = {
firstName: "John"
};
let user2 = null;
alert( user1?.[key] ); // John
alert( user2?.[key] ); // undefined
此外,我们还可以将 ?.
跟 delete
一起使用:
delete user?.name; // 如果 user 存在,则删除 user.name
⚠️我们可以使用
?.
来安全地读取或删除,但不能写入可选链
?.
不能用在赋值语句的左侧。例如:
let user = null; user?.name = "John"; // Error,不起作用 // 因为它在计算的是:undefined = "John"
总结
可选链 ?.
语法有三种形式:
obj?.prop
—— 如果obj
存在则返回obj.prop
,否则返回undefined
。obj?.[prop]
—— 如果obj
存在则返回obj[prop]
,否则返回undefined
。obj.method?.()
—— 如果obj.method
存在则调用obj.method()
,否则返回undefined
。
正如我们所看到的,这些语法形式用起来都很简单直接。?.
检查左边部分是否为 null/undefined
,如果不是则继续运算。
?.
链使我们能够安全地访问嵌套属性。
但是,我们应该谨慎地使用 ?.
,根据我们的代码逻辑,仅在当左侧部分不存在也可接受的情况下使用为宜。以保证在代码中有编程上的错误出现时,也不会对我们隐藏。
- 如果对大家有帮助,请三连支持一下!
- 有问题欢迎评论区留言,及时帮大家解决!
- 点赞
- 收藏
- 关注作者
评论(0)