【JavaScript 从入门到精通】构造器|可选链

举报
xcc-2022 发表于 2022/09/16 20:25:12 2022/09/16
【摘要】 @[toc] 构造器和操作符 “new”常规的 {...} 语法允许创建一个对象。但是我们经常需要创建很多类似的对象,例如多个用户或菜单项等。这可以使用构造函数和 "new" 操作符来实现。 构造函数构造函数在技术上是常规函数。不过有两个约定:它们的命名以大写字母开头。它们只能由 "new" 操作符来执行。例如:function User(name) { this.name = name;...

@[toc]

构造器和操作符 “new”

常规的 {...} 语法允许创建一个对象。但是我们经常需要创建很多类似的对象,例如多个用户或菜单项等。

这可以使用构造函数和 "new" 操作符来实现。

构造函数

构造函数在技术上是常规函数。不过有两个约定:

  1. 它们的命名以大写字母开头。
  2. 它们只能由 "new" 操作符来执行。

例如:

function User(name) {
  this.name = name;
  this.isAdmin = false;
}

let user = new User("Jack");

alert(user.name); // Jack
alert(user.isAdmin); // false

当一个函数被使用 new 操作符执行时,它按照以下步骤:

  1. 一个新的空对象被创建并分配给 this
  2. 函数体执行。通常它会修改 this,为其添加新的属性。
  3. 返回 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() 这样的函数 AB

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.addressundefined,尝试读取 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 相同,
  • 否则(当 valueundefined/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 中,?. 允许 usernull/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"

总结

可选链 ?. 语法有三种形式:

  1. obj?.prop —— 如果 obj 存在则返回 obj.prop,否则返回 undefined
  2. obj?.[prop] —— 如果 obj 存在则返回 obj[prop],否则返回 undefined
  3. obj.method?.() —— 如果 obj.method 存在则调用 obj.method(),否则返回 undefined

正如我们所看到的,这些语法形式用起来都很简单直接。?. 检查左边部分是否为 null/undefined,如果不是则继续运算。

?. 链使我们能够安全地访问嵌套属性。

但是,我们应该谨慎地使用 ?.,根据我们的代码逻辑,仅在当左侧部分不存在也可接受的情况下使用为宜。以保证在代码中有编程上的错误出现时,也不会对我们隐藏。

  • 如果对大家有帮助,请三连支持一下!
  • 有问题欢迎评论区留言,及时帮大家解决!

在这里插入图片描述

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。