First floor – 3 JavaScript

Java?JavaScript!

JavaScript
大部分抄自现代js指南QwQ

JavaScript 引擎

浏览器中嵌入了 JavaScript 引擎,有时也称作“JavaScript 虚拟机”。
不同的引擎有不同的“代号”,例如:

  • V8 —— Chrome、Opera 和 Edge 中的 JavaScript 引擎。
  • SpiderMonkey —— Firefox 中的 JavaScript 引擎。
  • “Chakra” 用于 IE,“JavaScriptCore”、“Nitro” 和 “SquirrelFish” 用于 Safari,等等。

引擎是如何工作的?
引擎很复杂,但是基本原理很简单。

  1. 引擎(如果是浏览器,则引擎被嵌入在其中)读取(“解析”)脚本。
  2. 然后,引擎将脚本转化(“编译”)为机器语言。
  3. 然后,机器代码快速地执行。
  4. 引擎会对流程中的每个阶段都进行优化。它甚至可以在编译的脚本运行时监视它,分析流经该脚本的数据,并根据获得的信息进一步优化机器代码。

浏览器中的JavaScript

js是一种“安全的”编程语言,不提供对内存或CPU的底层访问。

  • 网页中的 JavaScript 不能读、写、复制和执行硬盘上的任意文件。它没有直接访问操作系统的功能。
  • 现代浏览器允许 JavaScript 做一些文件相关的操作,但是这个操作是受到限制的。仅当用户做出特定的行为,JavaScript 才能操作这个文件。例如,用户把文件“拖放”到浏览器中,或者通过 <input> 标签选择了文件。
  • 有很多与相机/麦克风和其它设备进行交互的方式,但是这些都需要获得用户的明确许可。因此,启用了 JavaScript 的网页应该不会偷偷地启动网络摄像头观察你,并把你的信息发送到 美国国家安全局。(lol)
  • 不同的标签页/窗口之间通常互不了解。有时候,也会有一些联系,例如一个标签页通过 JavaScript 打开的另外一个标签页。但即使在这种情况下,如果两个标签页打开的不是同一个网站(域名、协议或者端口任一不相同的网站),它们都不能相互通信。
  • 这就是所谓的“同源策略”。为了解决“同源策略”问题,两个标签页必须  包含一些处理这个问题的特定的 JavaScript 代码,并均允许数据交换。本教程会讲到这部分相关的知识。
  • 这个限制也是为了用户的信息安全。例如,用户打开的 http://anysite.com 网页必须不能访问 http://gmail.com(另外一个标签页打开的网页)也不能从那里窃取信息。
  • JavaScript 可以轻松地通过互联网与当前页面所在的服务器进行通信。但是从其他网站/域的服务器中接收数据的能力被削弱了。尽管可以,但是需要来自远程服务器的明确协议(在 HTTP header 中)。这也是为了用户的信息安全。

当然在浏览器环境外,比如服务器上,不存在此类限制。

JavaScript “上层”语言

这些语言在浏览器中执行之前,都会被编译(转化)成 JavaScript。

  • CoffeeScript 是 JavaScript 的一种语法糖。它引入了更加简短的语法,使我们可以编写更清晰简洁的代码。通常,Ruby 开发者喜欢它。
  • TypeScript 专注于添加“严格的数据类型”以简化开发,以更好地支持复杂系统的开发。由微软开发。
  • Flow 也添加了数据类型,但是以一种不同的方式。由 Facebook 开发。
  • Dart 是一门独立的语言。它拥有自己的引擎,该引擎可以在非浏览器环境中运行(例如手机应用),它也可以被编译成 JavaScript。由 Google 开发。
  • Brython 是一个 Python 到 JavaScript 的转译器,让我们可以在不使用 JavaScript 的情况下,以纯 Python 编写应用程序。
  • Kotlin 是一个现代、简洁且安全的编程语言,编写出的应用程序可以在浏览器和 Node 环境中运行。

规范

ECMA-262 规范 包含了大部分深入的、详细的、规范化的关于 JavaScript 的信息。这份规范明确地定义了这门语言。
但正因其规范化,对于新手来说难以理解。所以,如果你需要关于这门语言细节最权威的信息来源,这份规范就很适合你(去阅读)。但它并不适合日常使用。
每年都会发布一个新版本的规范。最新的规范草案请见 https://tc39.es/ecma262/
想了解最新最前沿的功能,包括“即将纳入规范的”(所谓的 “stage 3”),请看这里的提案 https://github.com/tc39/proposals

手册

  • MDN(Mozilla)JavaScript 索引 是一个带有用例和其他信息的主要的手册。它是一个获取关于个别语言函数、方法等深入信息的很好的信息来源。你可以在 此处阅读
    不过,利用互联网搜索通常是最好的选择。只需在查询时输入“MDN [关键字]”,例如 https://google.com/search?q=MDN+parseInt 搜索 parseInt 函数。

开发者控制台 F12!

多行输入
通常,当我们向控制台输入一行代码后,按 Enter,这行代码就会立即执行。
如果想要插入多行代码,请按 Shift+Enter 来进行换行。这样就可以输入长片段的 JavaScript 代码了。

JavaScript 基础

<script> 标签

如果设置了 src 特性,script 标签内容将会被忽略。
一个单独的 <script> 标签不能同时有 src 特性和内部包裹的代码。

分号

省流:给我加分号

可以不加分号,JavaScript 将换行符理解成“隐式”的分号。这也被称为 自动分号插入
在大多数情况(不总是)下,换行意味着一个分号。

alert(3 + 
	  1 
	  + 2);

JavaScript 并没有在这里插入分号。
HOWEVER
存在 JavaScript 无法确定是否真的需要自动插入分号的情况。

alert("Hello");
[1, 2].forEach(alert);
//正常运行,输出Hello 1 2
alert("Hello")
[1, 2].forEach(alert);
//只输出Hello,并有报错
alert("Hello")[1, 2].forEach(alert);
//在引擎看来是这样的

注释

//单行
/*
多行
*/
使用快捷键!
在大多数的编辑器中,一行代码可以使用 Ctrl+/ 快捷键进行单行注释,诸如 Ctrl+Shift+/ 的快捷键可以进行多行注释(选择代码,然后按下快捷键)。
不支持注释嵌套!
不要在 /*...*/ 内嵌套另一个 /*...*/

/*
	/*错误*/
*/
alert('world')

可以看到最后一个*/被识别到注释外。

现代模式 "use strict"

省流:欢迎将 "use strict"; 写在脚本的顶部。当你的代码全都写在了 class 和 module 中时,则可以将 "use strict"; 省略掉。

ES5 规范增加了新的语言特性并且修改了一些已经存在的特性。为了保证旧的功能能够使用,大部分的修改是默认不生效的。你需要一个特殊的指令 —— "use strict" 来明确地激活这些特性。
这个指令看上去像一个字符串 "use strict" 或者 'use strict'。当它处于脚本文件的顶部时,则整个脚本文件都将以“现代”模式进行工作。

确保 “use strict” 出现在最顶部
请确保 "use strict" 出现在脚本的最顶部,否则严格模式可能无法启用。
只有注释可以出现在 "use strict" 的上面。

没有回头路,没有办法取消 use strict
没有类似于 "no use strict" 这样的指令可以使程序返回默认模式。
一旦进入了严格模式,就没有回头路了。

如何在浏览器中使用。
开发者控制台默认不启动use strict。
使用 Shift+Enter 按键去输入多行代码,然后将 use strict 放在代码最顶部。
很丑但可靠的启用 use strict 的方法。like this

(function() {
  'use strict';

  // ...你的代码...
})()

现代 JavaScript 支持 “class” 和 “module” —— 高级语言结构,它们会自动启用 use strict。因此,如果我们使用它们,则无需添加 "use strict" 指令。

变量 let

老版本中存在var,大体相同,后续介绍

函数式语言
有趣的是,也存在禁止更改变量值的 函数式 编程语言。比如 Scala 或 Erlang
在这种类型的语言中,一旦值保存在盒子中,就永远存在。如果你试图保存其他值,它会强制创建一个新盒子(声明一个新变量),无法重用之前的变量。
虽然第一次看上去有点奇怪,但是这些语言有很大的发展潜力。不仅如此,在某些领域,比如并行计算,这个限制有一定的好处。研究这样的一门语言(即使不打算很快就用上它)有助于开阔视野。

命名

  1. 变量名称必须仅包含字母、数字、符号 $ 和 _
  2. 首字符必须非数字。
    大小写敏感
    支持其他语言,但不建议
    保留字。。。

无严格模式
变量不需要提前声明

常量 const

普遍做法:大写命名,记住那些在执行之前就已知的难以记住的值。
有些在执行期间被“计算”出来,但初始赋值之后就不会改变。

const pageLoadTime = /* 网页加载所需的时间 */;

pageLoadTime 的值在页面加载之前是未知的,所以采用常规命名。但是它仍然是个常量,因为赋值之后不会改变。
换句话说,大写命名的常量仅用作“硬编码(hard-coded)”值的别名。

重用还是新建?

最后一点,有一些懒惰的程序员,倾向于重用现有的变量,而不是声明一个新的变量。
结果是,这个变量就像是被扔进不同东西盒子,但没有改变它的贴纸。现在里面是什么?谁知道呢。我们需要靠近一点,仔细检查才能知道。
这样的程序员节省了一点变量声明的时间,但却在调试代码的时候损失数十倍时间。
额外声明一个变量绝对是利大于弊的。
现代的 JavaScript 压缩器和浏览器都能够很好地对代码进行优化,所以不会产生性能问题。为不同的值使用不同的变量可以帮助引擎对代码进行优化。

数据类型

我们可以将任何类型的值存入同一变量。
允许这种操作的编程语言,例如 JavaScript,被称为“动态类型”(dynamically typed)的编程语言,意思是虽然编程语言中有不同的数据类型,但是你定义的变量并不会在定义后,被限制为某一数据类型。

Number

“特殊数值(“special numeric values”)”也属于这种类型:Infinity、-Infinity 和 NaN。
Infinity(无限):除0得到或直接使用
NaN:计算错误,不正确的或者一个未定义的数学操作所得到的结果,NaN 是粘性的。任何对 NaN 的进一步数学运算都会返回 NaN
如果在数学表达式中有一个 NaN,会被传播到最终结果(只有一个例外:NaN ** 0 结果为 1)。

数学运算是安全的
在 JavaScript 中做数学运算是安全的。我们可以做任何事:除以 0,将非数字字符串视为数字,等等。
脚本永远不会因为一个致命的错误(“死亡”)而停止。最坏的情况下,我们会得到 NaN 的结果。

Bigint

在 JavaScript 中,“number” 类型无法安全地表示大于 (2^53-1)(即 9007199254740991),或小于 -(2^53-1) 的整数。
更准确的说,“number” 类型可以存储更大的整数(最多 1.7976931348623157 * 10^308),但超出安全整数范围 ±(2^53-1) 会出现精度问题,因为并非所有数字都适合固定的 64 位存储。因此,可能存储的是“近似值”。

BigInt 类型是最近被添加到 JavaScript 语言中的,用于表示任意长度的整数。
可以通过将 n 附加到整数字段的末尾来创建 BigInt 值。

// 尾部的 "n" 表示这是一个 BigInt 类型
const bigInt = 1234567890123456789012345678901234567890n;

String

在 JavaScript 中,有三种包含字符串的方式。

  1. 双引号:"Hello".
  2. 单引号:'Hello'.
  3. 反引号:`Hello`.
    双引号和单引号都是“简单”引用,在 JavaScript 中两者几乎没有什么差别。
    反引号是 功能扩展 引号。它们允许我们通过将变量和表达式包装在 ${…} 中,来将它们嵌入到字符串中。
let name = "Levin";
alert(`Hello, ${name}!`);

JavaScript 中没有 character 类型。
在一些语言中,单个字符有一个特殊的 “character” 类型,在 C 语言和 Java 语言中被称为 “char”。
在 JavaScript 中没有这种类型。只有一种 string 类型,一个字符串可以包含零个(为空)、一个或多个字符。

Boolean

布尔值也可作为比较的结果

let isGreater = 4 > 1;

Null

特殊的 null 值不属于上述任何一种类型。
它构成了一个独立的类型,只包含 null 值
相比较于其他编程语言,JavaScript 中的 null 不是一个“对不存在的 object 的引用”或者 “null 指针”。
JavaScript 中的 null 仅仅是一个代表“无”、“空”或“值未知”的特殊值。

Undefined

特殊值 undefined 和 null 一样自成类型。
undefined 的含义是 未被赋值
如果一个变量已被声明,但未被赋值,那么它的值就是 undefined

let age;
alert(age); // 弹出 "undefined"

从技术上讲,可以显式地将 undefined 赋值给变量……但是不建议这样做。通常,使用 null 将一个“空”或者“未知”的值写入变量中,而 undefined 则保留作为未进行初始化的事物的默认初始值。

Object Symbol

object 类型是一个特殊的类型。
其他所有的数据类型都被称为“原始类型”,因为它们的值只包含一个单独的内容(字符串、数字或者其他)。相反,object 则用于储存数据集合和更复杂的实体。

symbol 类型用于创建对象的唯一标识符。我们在这里提到 symbol 类型是为了完整性,但我们要在学完 object 类型后再学习它。

typeof 运算符

typeof x | typeof(x)

typeof null 的结果为 "object"。这是官方承认的 typeof 的错误,这个问题来自于 JavaScript 语言的早期阶段,并为了兼容性而保留了下来。null 绝对不是一个 objectnull 有自己的类型,它是一个特殊值。typeof 的行为在这里是错误的。

typeof alert 的结果是 "function",因为 alert 在 JavaScript 语言中是一个函数。在 JavaScript 语言中没有一个特别的 “function” 类型。函数隶属于 object 类型。但是 typeof 会对函数区分对待,并返回 "function"。这也是来自于 JavaScript 语言早期的问题。从技术上讲,这种行为是不正确的,但在实际编程中却非常方便。

交互 alert prompt confirm

alert

它会显示一条信息,并等待用户按下 “OK”。
例如:
alert("Hello");
弹出的这个带有信息的小窗口被称为 模态窗。“modal” 意味着用户不能与页面的其他部分(例如点击其他按钮等)进行交互,直到他们处理完窗口。在上面示例这种情况下 —— 直到用户点击“确定”按钮。

prompt

prompt 函数接收两个参数:
result = prompt(title, [default]);
浏览器会显示一个带有文本消息的模态窗口,还有 input 框和确定/取消按钮。
title
显示给用户的文本
default
可选的第二个参数,指定 input 框的初始值。

上述语法中 default 周围的方括号表示该参数是可选的,不是必需的。

访问者可以在提示输入栏中输入一些内容,然后按“确定”键。然后我们在 result 中获取该文本。或者他们可以按取消键或按 Esc 键取消输入,然后我们得到 null 作为 result

IE 浏览器会提供默认值
第二个参数是可选的。但是如果我们不提供的话,Internet Explorer 会把 "undefined" 插入到 prompt。
所以,为了 prompt 在 IE 中有好的效果,我们建议始终提供第二个参数:

let test = prompt("test", '');

confirm

语法:
result = confirm(question);
confirm 函数显示一个带有 question 以及确定和取消两个按钮的模态窗口。
点击确定返回 true,点击取消返回 false

这些方法都是模态的:它们暂停脚本的执行,并且不允许用户与该页面的其余部分进行交互,直到窗口被解除。
上述所有方法共有两个限制:

  1. 模态窗口的确切位置由浏览器决定。通常在页面中心。
  2. 窗口的确切外观也取决于浏览器。我们不能修改它。

类型转换

大多数情况下,运算符和函数会自动将赋予它们的值转换为正确的类型。

值得注意:

  • 对 undefined 进行数字型转换时,输出结果为 NaN,而非 0
  • 对 "0" 和只有空格的字符串(比如:" ")进行布尔型转换时,输出结果为 true

字符串

alert()会将自动进行字符串转换。

value = String(value); 

false 变成 "false"null 变成 "null"

数字

在算术函数和表达式中,会自动进行 number 类型转换。

num = Number(str);

如果该字符串不是一个有效的数字,转换的结果会是NaN
转换规则

变成……
undefined NaN
null 0
true 和 false 1 and 0
string 去掉首尾空白字符(空格、换行符 \n、制表符 \t 等)后的纯数字字符串中含有的数字。如果剩余字符串为空,则转换结果为 0。否则,将会从剩余字符串中“读取”数字。当类型转换出现 error 时返回 NaN
alert( Number("   123   ") ); // 123
// NaN(从字符串“读取”数字,读到 "z" 时出现错误,返回NaN)
alert( Number("123z") );

布尔型

发生在逻辑运算中(条件判断和其他类似的东西)
转换规则

  • 直观上为“空”的值(如 0、空字符串、nullundefined 和 NaN)将变为 false
  • 其他值变成 true
    注意!
    字符串"0"," "是true,在 JavaScript 中,非空的字符串总是 true

运算符

和python差不多,
取余%,求幂 **
+可以连接字符串(只要有一边是string另一边也会转换为string,仅加法会这样转换)(*不可以用于重复字符串)
+可以用于转换数字

y = +true; // 1

根据以上特性,有些情况下须这样使用:

// apples:"2" oranges:"3"
+apples + +oranges

等号运算符,返回右边的值(知道即可,少写)

let a = 1;
let b = 2;
let c = 3 - (a = b + 1);
//c = 0

链式赋值

a = b = c = 2 + 2;//a = b = c = 4

位运算符把运算元当做 32 位整数,并在它们的二进制表现形式上操作。
这些运算符不是 JavaScript 特有的。大部分的编程语言都支持这些运算符。
下面是位运算符:

  • 按位与 ( & )
  • 按位或 ( | )
  • 按位异或 ( ^ )
  • 按位非 ( ~ )
  • 左移 ( << )
  • 右移 ( >> )
  • 无符号右移 ( >>> )
    这些运算符很少被使用

逗号运算符只返回最后一个表达式的结果
请注意逗号运算符的优先级非常低,比 = 还要低

逻辑运算符

||
result = value1 || value2 || value3;

或运算符 || 做如下的事情:

  • 从左到右依次计算操作数。
  • 处理每一个操作数时,都将其转化为布尔值。如果结果是 true,就停止计算,返回这个操作数的初始值。
  • 如果所有的操作数都被计算过(也就是,转换结果都是 false),则返回最后一个操作数。
    返回的值是操作数的初始形式,不会做布尔转换。
    根据以上特性,诞生了许多有趣的用法
    1. 获取变量列表或者表达式中的第一个真值
let firstName = "";
let lastName = "";
let nickName = "SuperCoder";

alert( firstName || lastName || nickName || "Anonymous");
// SuperCoder

2. 短路求值(Short-circuit evaluation)
或运算符 || 的另一个用途是所谓的“短路求值”。
这指的是,|| 对其参数进行处理,直到达到第一个真值,然后立即返回该值,而无需处理其他参数。
如果操作数不仅仅是一个值,而是一个有副作用的表达式,例如变量赋值或函数调用,那么这一特性的重要性就变得显而易见了。

true || alert("not printed");
false || alert("printed");
&&

类似地,与运算符寻找第一个假值
与运算 && 做如下的事:

  • 从左到右依次计算操作数。
  • 在处理每一个操作数时,都将其转化为布尔值。如果结果是 false,就停止计算,并返回这个操作数的初始值。
  • 如果所有的操作数都被计算过(例如都是真值),则返回最后一个操作数。

与运算 && 在或运算 || 之前进行

最好不要用 || 或 && 来取代 if
有时候,有人会将与运算符 && 作为“简化 if”的一种方式。

两个非运算 !! 有时候用来将某个值转化为布尔类型

空值合并运算符 '??'

当一个值既不是 null 也不是 undefined 时,我们将其称为“已定义的(defined)”。

  • ?? 返回第一个 已定义的 值。一种获得两者中的第一个“已定义的”值的不错的语法。
    ?? 的常见使用场景是提供默认值。
  • || 返回第一个  值。
  • ?? 返回第一个 已定义的 值。

出于安全原因,JavaScript 禁止将 ?? 运算符与 && 和 || 运算符一起使用,除非使用括号明确指定了优先级。会触发语法错误。

比较(条件判断)

try

5 > 4 → true
"apple" > "pineapple" → false
"2" > "12" → true
undefined == null → true
undefined === null → false
null == "\n0\n" → false
null === +"\n0\n" → false

throw

  1. 数字间比较大小,显然得 true。
  2. 按词典顺序比较,得 false。"a" 比 "p" 小。
  3. 与第 2 题同理,首位字符 "2" 大于 "1"
  4. null 只与 undefined 互等。
  5. 严格相等模式下,类型不同得 false。
  6. 与第 4 题同理,null 只与 undefined 相等。
  7. 不同类型严格不相等。

memes:

Pasted image 20240419165200.png

有点东西|没了东西
没放东西|啥东西?

和c系代码类似

字符串比较,字典序(准确说是Unicode 编码顺序)

不同类型比较,将会转换为number

一个有趣的现象
有时候,以下两种情况会同时发生:

  • 若直接比较两个值,其结果是相等的。
  • 若把两个值转为布尔值,它们可能得出完全相反的结果,即一个是 true,一个是 false
  • 例如:
let a = 0;
alert( Boolean(a) ); // false

let b = "0";
alert( Boolean(b) ); // true

alert(a == b); // true!

对于 JavaScript 而言,这种现象其实挺正常的。因为 JavaScript 会把待比较的值转化为数字后再做比较(因此 "0" 变成了 0)。若只是将一个变量转化为 Boolean 值,则会使用其他的类型转换规则。

严格(不)相等

//无法区分空字符串和0和false,因为都被转换为了0
0 == false // true
'' == false // true

// 若属于不同的数据类型,那么不会做任何的类型转换而立刻返回 `false`。
0 === false // false

严格相等运算符 === 在进行比较时不会做任何的类型转换。
**同样地:严格不相等运算符 !== **

null 与 undefined

null === undefined // false
null == undefined // true

使用数学式或其他比较方法 < > <= >= 时:
null/undefined 会被转化为数字:null 被转化为 0undefined 被转化为 NaN

奇怪的结果 null vs 0
null > 0 // false(1)
null == 0 // false(2)
null >= 0 // true(3)

what happened?
因为相等性检查 == 和普通比较符 > < >= <= 的代码逻辑是相互独立的。进行值的比较时,null 会被转化为数字,因此它被转化为了 0。这就是为什么(3)中 null >= 0 返回值是 true,(1)中 null > 0 返回值是 false。
另一方面,undefined 和 null 在相等性检查 == 中不会进行任何的类型转换,它们有自己独立的比较规则,所以除了它们之间互等外,不会等于任何其他的值。这就解释了为什么(2)中 null == 0 会返回 false。

特立独行的 undefined

undefined 不应该被与其他值进行比较:

undefined > 0 // false (1)
undefined < 0 // false (2)
undefined == 0 // false (3)

为何它看起来如此厌恶 0?

  • (1) 和 (2) 都返回 false 是因为 undefined 在比较中被转换为了 NaN,而 NaN 是一个特殊的数值型值,它与任何值进行比较都会返回 false
  • (3) 返回 false 是因为这是一个相等性检查,而 undefined 只与 null 相等,不会与其他值相等。

条件分支

if (condition)
{
	command;
}

类c
?运算符,可套娃

let result = condition ? value1 : value2;

?的非常规使用

let company = prompt('Which company created JavaScript?', '');

(company == 'Netscape') ?
   alert('Right!') : alert('Wrong.');

switch:
任何表达式都可以成为 switch/case 的参数
switch 的相等判断是严格相等(===)

循环

类c
一些特点

禁止 break/continue 在 ‘?’ 的右边
请注意非表达式的语法结构不能与三元运算符 ? 一起使用。特别是 break/continue 这样的指令是不允许这样使用的。

if (i > 5) {
  alert(i);
} else {
  continue;
}

(i > 5) ? alert(i) : continue; // continue 不允许在这个位置

break/continue多层循环只能跳一级的问题,标签诞生了!

labelName: for (...) {
  ...
}
outer: for (let i = 0; i < 3; i++) {

  for (let j = 0; j < 3; j++) {

    let input = prompt(`Value at coords (${i},${j})`, '');

    // 如果是空字符串或被取消,则中断并跳出这两个循环。
    if (!input) break outer; // (*)

    // 用得到的值做些事……
  }
}

alert('Done!');

标签并不允许“跳到”所有位置
标签不允许我们跳到代码的任意位置。

for...of/for...in

#TODO

函数

SIMPLE

function func(parameter1, parameter1 = xxx/func()) {
	...
	return xxx;
}

func(args)

只有在没有局部变量的情况下才会使用外部变量。
如果在函数内部声明了同名变量,那么函数会 遮蔽(外部变量跟局部变量重名,不应该称之屏蔽外部变量,而是js变量解析机制,从最近的局部变量->往上寻找,直到全局变量,找到即为结束) 外部变量。

  • 参数(parameter)是函数声明中括号内列出的变量(它是函数声明时的术语)。
  • 参数(argument)是调用函数时传递给函数的值(它是函数调用时的术语)。

默认值:如果一个函数被调用,但有参数(argument)未被提供,那么相应的值就会变成 undefined
我们可以使用 = 为函数声明中的参数指定所谓的“默认”(如果对应参数的值未被传递则使用)值。
返回值:空值的return(等效return undefined)或没有return的函数返回值为undefined

不要在 return 与返回值之间添加新行
对于 return 的长表达式,可能你会很想将其放在单独一行,如下

return
 (some + long + expression + or + whatever * f(a) + f(b))

但这不行,因为 JavaScript 默认会在 return 之后加上分号。

return;
 (some + long + expression + or + whatever * f(a) + f(b))

因此,实际上它的返回值变成了空值。
如果我们想要将返回的表达式写成跨多行的形式,那么应该在 return 的同一行开始写此表达式。或者按照如下的方式放上左括号:

return (
  some + long + expression
  + or +
  whatever * f(a) + f(b)
  )

命名
以 "show" 开头的函数通常会显示某些内容。
函数以 XX 开始……

  • "get…" —— 返回一个值,
  • "calc…" —— 计算某些内容,
  • "create…" —— 创建某些内容,
  • "check…" —— 检查某些内容并返回 boolean 值,等。

函数表达式

函数也是值,允许我们在任何表达式的中间创建一个新函数。

//正常声明的函数名也是和下面一样的值
let func = function(parameter1, parameter1 = xxx/func()) {
	...
	return xxx;
};

函数表达式允许省略函数名。
注意最后的分号
调用alert(func)时,输出函数源码(不会运行函数),调用alert(func())时才会运行函数。

回调函数 & 匿名函数:

function ask(question, yes, no) {
  if (confirm(question)) yes()
  else no();
}

function showOk() {
  alert( "You agreed." );
}

function showCancel() {
  alert( "You canceled the execution." );
}

// 用法:函数 showOk 和 showCancel 被作为参数传入到 ask
ask("Do you agree?", showOk, showCancel);

//--------------------
function ask(question, yes, no) {
  if (confirm(question)) yes()
  else no();
}

ask(
  "Do you agree?",
  function() { alert("You agreed."); },
  function() { alert("You canceled the execution."); }
);

ask 的两个参数值 showOk 和 showCancel 可以被称为 回调函数 或简称 回调
主要思想是我们传递一个函数,并期望在稍后必要时将其“回调”。在我们的例子中,showOk 是回答 “yes” 的回调,showCancel 是回答 “no” 的回调。

第二种写法:直接在 ask(...) 调用内进行函数声明。这两个函数没有名字,所以叫 匿名函数。这样的函数在 ask 外无法访问(因为没有对它们分配变量),不过这正是我们想要的。
这样的代码在我们的脚本中非常常见,这正符合 JavaScript 语言的思想。

一个函数是表示一个“行为”的值
字符串或数字等常规值代表 数据
函数可以被视为一个 行为(action)
我们可以在变量之间传递它们,并在需要时运行。

函数表达式是在代码执行到达时被创建,并且仅从那一刻起可用。
函数声明在函数声明被定义之前,它就可以被调用。
严格模式下,当一个函数声明在一个代码块内时,它在该代码块内的任何位置都是可见的。但在代码块外不可见。

箭头函数

let func = (parameter1, parameter2) => expression;
let func = function(parameter1, parameter2)
{
	return expression;
}
//
let func = parameter1 => expression;
let func = () => expression;
//
let func = (parameter1, parameter2) => {
	other...
	return expression;
};

JavaScript 特性总结

代码质量

介绍在开发中进一步使用的编码实践

开发者工具-在浏览器中调试

开发人员工具中的选项比本文介绍的多得多。完整的手册请点击这个链接查看:https://developers.google.com/web/tools/chrome-devtools

资源(Sources)面板

Pasted image 20240421104002.png

资源(Sources)面板包含三个部分:

  1. 文件导航(File Navigator) 区域列出了 HTML、JavaScript、CSS 和包括图片在内的其他依附于此页面的文件。Chrome 扩展程序也会显示在这。
  2. 代码编辑(Code Editor) 区域展示源码。
  3. JavaScript 调试(JavaScript Debugging) 区域是用于调试的,我们很快就会来探索它。

控制台(Console)

输入命令执行,在下一行显示返回值

断点(Breakpoints)

点击行号即可设置断点,
我们总是可以在右侧的面板中找到断点的列表。当我们在数个文件中有许多断点时,这是非常有用的。它允许我们:

  • 快速跳转至代码中的断点(通过点击右侧面板中的对应的断点)。
  • 通过取消选中断点来临时禁用对应的断点。
  • 通过右键单击并选择移除来删除一个断点。
  • ……等等。

条件断点
在行号上 右键单击 允许你创建一个 条件 断点。只有当给定的表达式(你创建条件断点时提供的表达式)为真时才会被触发。
当我们需要在特定的变量值或参数的情况下暂停程序执行时,这种调试方法就很有用了。

Pasted image 20240421111506.png

debugger 命令

function hello(name) {
	let phrase = `Hello, ${name}!`;

	debugger; // 调试器会在此暂停

	alert(phrase);
}

这种命令只有在开发者工具打开时才有效,否则浏览器会忽略它。

暂停并查看

[[First floor - 3 JavaScript#资源(Sources)面板]]
设置好断点后,即可刷新页面
请打开右侧的信息下拉列表(箭头指示出的地方)。这里允许你查看当前的代码状态:

  1. 察看(Watch) —— 显示任意表达式的当前值。
    你可以点击加号 + 然后输入一个表达式。调试器将显示它的值,并在执行过程中自动重新计算该表达式。
  2. 调用栈(Call Stack) —— 显示嵌套的调用链。
    此时,调试器正在 hello() 的调用链中,被 index.html 中的一个脚本调用(这里没有函数,因此显示 “anonymous”)
    如果你点击了一个堆栈项,调试器将跳到对应的代码处,并且还可以查看其所有变量。
  3. 作用域(Scope) —— 显示当前的变量。
    Local 显示当前函数中的变量,你还可以在源代码中看到它们的值高亮显示了出来。
    Global 显示全局变量(不在任何函数中)。

跟踪执行

“恢复(Resume)”:继续执行,快捷键 F8。
继续执行。如果没有其他的断点,那么程序就会继续执行,并且调试器不会再控制程序。

“下一步(Step)”:运行下一条(即当前行)指令,快捷键 F9。
运行下一条语句。如果我们现在点击它,alert 会被显示出来。
一次接一次地点击此按钮,整个脚本的所有语句会被逐个执行。

“跨步(Step over)”:运行下一条(即当前行)指令,但 不会进入到一个函数中,快捷键 F10。
跟上一条命令“下一步(Step)”类似,但如果下一条语句是函数调用则表现不同。这里的函数指的是:不是内建的如 alert 函数等,而是我们自己写的函数。
如果我们对比一下,“下一步(Step)”命令会进入嵌套函数调用并在其第一行暂停执行,而“跨步(Step over)”对我们不可见地执行嵌套函数调用,跳过了函数内部。
执行会在该函数调用后立即暂停。
如果我们对该函数的内部执行不感兴趣,这命令会很有用。

“步入(Step into)”,快捷键 F11。
和“下一步(Step)”类似,但在异步函数调用情况下表现不同。如果你刚刚才开始学 JavaScript,那么你可以先忽略此差异,因为我们还没有用到异步调用。
至于之后,只需要记住“下一步(Step)”命令会忽略异步行为,例如 setTimeout(计划的函数调用),它会过一段时间再执行。而“步入(Step into)”会进入到代码中并等待(如果需要)。详见 DevTools 手册

“步出(Step out)”:继续执行到当前函数的末尾,快捷键 Shift+F11。
继续执行当前函数内的剩余代码,并暂停在调用当前函数的下一行代码处。当我们使用  偶然地进入到一个嵌套调用,但是我们又对这个函数不感兴趣时,我们想要尽可能的继续执行到最后的时候是非常方便的。

启用/禁用所有的断点。
这个按钮不会影响程序的执行。只是一个批量操作断点的开/关。

启用/禁用出现错误时自动暂停脚本执行。
当启动此功能,如果开发者工具是打开着的时候,任何脚本执行错误都会导致该脚本执行自动暂停。然后我们可以在调试器中分析变量来看一下什么出错了。因此如果我们的脚本因为错误挂掉的时候,我们可以打开调试器,启用这个选项然后重载页面,查看一下哪里导致它挂掉了和当时的上下文是什么。

Continue to here
在代码中的某一行上右键,在显示的关联菜单(context menu)中点击一个非常有用的名为 “Continue to here” 的选项。
当你想要向前移动很多步到某一行为止,但是又懒得设置一个断点时非常的方便。

日志记录

console.log

代码风格

没有什么规则是“必须”的
没有什么规则是“刻在石头上”的。这些是风格偏好,而不是宗教教条。

代码风格 (javascript.info)

注释

“如果代码不够清晰以至于需要一个注释,那么或许它应该被重写。”

代码表示HOW,注释表示WHY

“解释性”注释:

// 这里的代码会先做这件事(……)然后做那件事(……)
// ……谁知道还有什么……
very;
complex;
code;

我们不能完全避免“解释型”注释。例如在一些复杂的算法中,会有一些出于优化的目的而做的一些巧妙的“调整”。

专门用于记录函数的语法 JSDoc:用法、参数和返回值。

/**
 * 返回 x 的 n 次幂的值。
 *
 * @param {number} x 要改变的值。
 * @param {number} n 幂数,必须是一个自然数。
 * @return {number} x 的 n 次幂的值。
 */
function pow(x, n) {
  ...
}

为什么不这样做?
通常注释给出了,一种实现功能的方法,但是有多种方法时,为什么选择这种而不是其他的往往更加重要。
没有这样的注释的话,就可能会发生下面的情况:

  1. 你(或者你的同事)打开了前一段时间写的代码,看到它不是最理想的实现方式。
  2. 你会想:“我当时是有多蠢啊,现在我真是太聪明了”,然后用“更显而易见且正确的”方式重写了一遍。
  3. ……重写的这股冲动劲是好的。但是在重写的过程中你发现“更显而易见”的解决方案实际上是有缺陷的。你甚至依稀地想起了为什么会这样,因为你很久之前就已经尝试过这样做了。于是你又还原了那个正确的实现,但是时间已经浪费了。

忍者代码

不多说,看就完了
忍者代码

自动化测试(浅浅了解)

使用 Mocha 进行自动化测试 (javascript.info)
自动化测试意味着测试是独立于代码的。它们以各种方式运行我们的函数,并将结果与预期结果进行比较。

describe("pow", function() {

  it("raises to n-th power", function() {
    assert.equal(pow(2, 3), 8);
  });

});

规范有三种使用方式:

  1. 作为 测试 —— 保证代码正确工作。
  2. 作为 文档 —— describe 和 it 的标题告诉我们函数做了什么。
  3. 作为 案例 —— 测试实际工作的例子展示了一个函数可以被怎样使用。

Polyfill 和 转译器

如何让我们现代的代码在还不支持最新特性的旧引擎上工作?
有两个工作可以做到这一点:

  1. 转译器(Transpilers)。
  2. 垫片(Polyfills)。

转译器

转译器是一种可以将源码转译成另一种源码的特殊的软件。它可以解析(“阅读和理解”)现代代码,并使用旧的语法结构对其进行重写,进而使其也可以在旧的引擎中工作。
例如,在 ES2020 之前没有“空值合并运算符” ??。所以,如果访问者使用过时了的浏览器访问我们的网页,那么该浏览器可能就不明白 height = height ?? 100 这段代码的含义。
转译器会分析我们的代码,并将 height ?? 100 重写为 (height !== undefined && height !== null) ? height : 100
通常,开发者会在自己的计算机上运行转译器,然后将转译后的代码部署到服务器。
说到名字,Babel 是最著名的转译器之一。
现代项目构建系统,例如 webpack,提供了在每次代码更改时自动运行转译器的方法,因此很容易将代码转译集成到开发过程中。

Polyfills

新的语言特性可能不仅包括语法结构和运算符,还可能包括内建函数。
例如,Math.trunc(n) 是一个“截断”数字小数部分的函数,例如 Math.trunc(1.23) 返回 1
在一些(非常过时的)JavaScript 引擎中没有 Math.trunc 函数,所以这样的代码会执行失败。
由于我们谈论的是新函数,而不是语法更改,因此无需在此处转译任何内容。我们只需要声明缺失的函数。
更新/添加新函数的脚本被称为“polyfill”。它“填补”了空白并添加了缺失的实现。
JavaScript 是一种高度动态的语言。脚本可以添加/修改任何函数,甚至包括内建函数。
两个有趣的 polyfill 库:

  • core js 支持了很多特性,允许只包含需要的特性。
  • polyfill.io 提供带有 polyfill 的脚本的服务,具体取决于特性和用户的浏览器。

对象

基础

类似字典

let user = new Object(); //“构造函数”的语法
let user = {}; //“字面量”的语法

let user = {
	name: "Levin",
	age: 20,
	"Likes something": true, // 多词属性名加引号
	key: value, //应以逗号结尾
	// 尾随(trailing)或悬挂(hanging)逗号。这样便于我们添加、删除和移动属性,因为所有的行都是相似的。
};

console.log(user.name); // Levin
console.log(user.age); // 20

user.isAdmin = true; // add 
delete user.age; // del

// 点运算符要求key是有效的变量标识符:包含空格,不以数字开头,也不包含特殊字符(允许使用 `$` 和 `_`)
// user.Likes something -- error
console.log(user["Likes something"])
// 方括号适用任何字符串

let key = "Likes something";
user[key] = true;
// 方括号优点


//计算属性
let fruit = prompt("Which fruit to buy?", "apple");
let bag = {
	[fruit]: 5,
};
alert(bag.apple);

//属性值简写,将已存在的变量当做属性名
function makeUser(name, age) {
	return {
		name, // 等同于 name: name
		age, // 等同于 age: age
	};
}

//属性名称限制,通常变量名不能是编程语言的保留字,如"for"等
//但对象的属性名不受限制,属性命名没有限制。属性名可以是任何字符串或者 symbol,其他类型会被自动地转换为字符串
let obj = {
	for: 1,
	let: 2,
	return: 3,
};

// a little trap: 名为__proto__的属性。不能将其设置为一个非对象的值
let obj = {};
obj.__proto__ = 5; // 此操作将被忽略
alert(obj.__proto__); // [object Object]
// 将在后续学习


// 属性存在性测试,in操作符,"key" in object
// 相比于其他语言,JavaScript 的对象能够被访问任何属性。即使属性不存在也不会报错,只会返回undefined!
let user = { name: "Levin" };
alert("name" in user) // true
alert("age" in user) // false
let name = "name" // 不加引号为变量名
alert(name in user) // true

// 为什么会存在in操作符,将属性与undefined比较不行吗,考虑以下情况:
let obj = {
	test: undefined
};
// 属性存在,但是undefined,这种情况很少发生,通常给变量赋值null而不是undefined

// for...in循环,循环变量可用key,prop
for (let key in user) {
	alert(key);
	alert(user[key]);
}

// 对象有顺序吗?
// “有特别的顺序”:整数属性会被进行排序,其他属性则按照创建的顺序显示。
let codes = {
  "49": "Germany",
  "41": "Switzerland",
  "44": "Great Britain",
  // ..,
  "1": "USA"
};
code["+86"] = "China";
code["-114"] = "514";

for(let code in codes) {
  alert(code); // 1, 41, 44, 49, +86, -114
}

// 整数属性?指的是一个可以在不做任何更改的情况下与一个整数进行相互转换的字符串。
// 如"49" 是一个整数属性名,因为我们把它转换成整数,再转换回来,它还是一样的。
String(Math.trunc(Number("49"))) == "49"

引用与复制

对象与原始类型的根本区别之一是,对象是“通过引用”存储和复制的,而原始类型:字符串、数字、布尔值等 —— 总是“作为一个整体”复制

let message = "Hello!";
let phrase = message;

Pasted image 20240422154209.png

let user = {
  name: "John"
};

Pasted image 20240422154241.png

let user = { name: "John" };
let admin = user; // 复制引用

Pasted image 20240422154313.png

let user = { name: 'John' };
let admin = user;
admin.name = 'Pete'; // 通过 "admin" 引用来修改
alert(user.name); // 'Pete',修改能通过 "user" 引用看到

通过引用比较

let a = {};
let b = a; // 复制引用

alert( a == b ); // true,都引用同一对象
alert( a === b ); // true
let a = {};
let b = {}; // 两个独立的对象

alert( a == b ); // false

克隆与合并 Object.assign(浅层拷贝)

let user = {
  name: "John",
  age: 30
};

let clone = {}; // 新的空对象

// 将 user 中所有的属性拷贝到其中
for (let key in user) {
  clone[key] = user[key];
}

// 现在 clone 是带有相同内容的完全独立的对象
clone.name = "Pete"; // 改变了其中的数据

alert( user.name ); // 原来的对象中的 name 属性依然是 John

or

let user = {
  name: "John",
  age: 30
};

let clone = Object.assign({}, user);

合并多个对象

// Object.assign(dest, [src1, src2, src3...])
let user = { name: "John" };

let permissions1 = { canView: true };
let permissions2 = { canEdit: true };

// 将 permissions1 和 permissions2 中的所有属性都拷贝到 user 中
Object.assign(user, permissions1, permissions2);

// 现在 user = { name: "John", canView: true, canEdit: true }

深层克隆

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

alert( user.sizes.height ); // 182
let clone = Object.assign({}, user);

alert( user.sizes === clone.sizes ); // true,同一个对象

// user 和 clone 分享同一个 sizes
user.sizes.width++;       // 通过其中一个改变属性值
alert(clone.sizes.width); // 51,能从另外一个获取到变更后的结果

为了解决这个问题,并让 user 和 clone 成为两个真正独立的对象,我们应该使用一个拷贝循环来检查 user[key] 的每个值,如果它是一个对象,那么也复制它的结构。这就是所谓的“深拷贝”。
我们可以使用递归来实现它。或者为了不重复造轮子,采用现有的实现,例如 lodash 库的 _.cloneDeep(obj)

使用 const 声明的对象也是可以被修改的
通过引用对对象进行存储的一个重要的副作用是声明为 const 的对象 可以 被修改。

const user = {
  name: "John"
};

user.name = "Pete"; // (*)

alert(user.name); // Pete

看起来 (*) 行的代码会触发一个错误,但实际并没有。user 的值是一个常量,它必须始终引用同一个对象,但该对象的属性可以被自由修改。
换句话说,只有当我们尝试将 user=... 作为一个整体进行赋值时,const user 才会报错。
也就是说,如果我们真的需要创建常量对象属性,也是可以的,但使用的是完全不同的方法。

垃圾回收

JavaScript 的内存管理是自动的、无形的。我们创建的原始值、对象、函数……这一切都会占用内存。
当我们不再需要某个东西时会发生什么?JavaScript 引擎如何发现它并清理它?
类似java
自动内存管理(垃圾回收)阵营:
JavaScript、Java、Go、Python、PHP、Ruby、C#
手动内存管理阵营:
C、C++、Rust

可达性

  1. 这里列出固有的可达值的基本集合,这些值明显不能被释放。
    • 当前执行的函数,它的局部变量和参数。
    • 当前嵌套调用链上的其他函数、它们的局部变量和参数。
    • 全局变量。
    • (还有一些内部的)
      这些值被称作 根(roots)
  2. 如果一个值可以通过引用链从根访问任何其他值,则认为该值是可达的。
    比方说,如果全局变量中有一个对象,并且该对象有一个属性引用了另一个对象,则  对象被认为是可达的。而且它引用的内容也是可达的。
    在 JavaScript 引擎中有一个被称作 垃圾回收器 的东西在后台执行。它监控着所有对象的状态,并删除掉那些已经不可达的。

算法 “mark-and-sweep”

定期执行以下“垃圾回收”步骤:

  • 垃圾收集器找到所有的根,并“标记”(记住)它们。
  • 然后它遍历并“标记”来自它们的所有引用。
  • 然后它遍历标记的对象并标记 它们的 引用。所有被遍历到的对象都会被记住,以免将来再次遍历到同一个对象。
  • ……如此操作,直到所有可达的(从根部)引用都被访问到。
  • 没有被标记的对象都会被删除。
    这是垃圾收集工作的概念。JavaScript 引擎做了许多优化,使垃圾回收运行速度更快,并且不会对代码执行引入任何延迟。
    一些优化建议:
  • 分代收集(Generational collection)—— 对象被分成两组:“新的”和“旧的”。在典型的代码中,许多对象的生命周期都很短:它们出现、完成它们的工作并很快死去,因此在这种情况下跟踪新对象并将其从内存中清除是有意义的。那些长期存活的对象会变得“老旧”,并且被检查的频次也会降低。
  • 增量收集(Incremental collection)—— 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟。因此,引擎将现有的整个对象集拆分为多个部分,然后将这些部分逐一清除。这样就会有很多小型的垃圾收集,而不是一个大型的。这需要它们之间有额外的标记来追踪变化,但是这样会带来许多微小的延迟而不是一个大的延迟。
  • 闲时收集(Idle-time collection)—— 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。

对象方法 "this"

方法简写

let user = {
	sayHi: function() {
		alert("hello");
	}
}

// |
// V

let user = {
	sayHi() {
		alert("hello");
	},
}

方法中的 "this"

通常,对象方法需要访问对象中存储的信息才能完成其工作。
为了访问该对象,方法中可以使用 this 关键字。

let user = {
	name: "Levin",
	sayHi() {
		alert("hello" + this.name);
	}
};

user.sayHi();

如果不用this用外部变量名来引用,是不可靠的,如果将该变量复制给另一个变量,将会访问到一个错误的对象。
到目前为止,this和大多数编程语言的this类似。

"this" 不受限制

JavaScript 中的 this 可以用于任何函数,即使它不是对象的方法。

// 无语法错误
function sayHi() {
	alert(this.name);
}

let user = { name: "Levin" };
let admin = { name: "Admin" };

user.f = sayHi;
admin.f = sayHi;
// 这两个调用有不同的 this 值
// 函数内部的 "this" 是“点符号前面”的那个对象
user.f(); //(this == user)
admin.f(); //(this == admin)

在没有对象的情况下调用:this == undefined

function sayHi() {
  alert(this);
}

sayHi(); // undefined

在这种情况下,严格模式下的 this 值为 undefined。如果我们尝试访问 this.name,将会报错。
在非严格模式的情况下,this 将会是 全局对象(浏览器中的 window,我们稍后会在 全局对象 一章中学习它)。这是一个历史行为,"use strict" 已经将其修复了。
通常这种调用是程序出错了。如果在一个函数内部有 this,那么通常意味着它是在对象上下文环境中被调用的。

解除 this 绑定的后果
如果你经常使用其他的编程语言,那么你可能已经习惯了“绑定 this”的概念,即在对象中定义的方法总是有指向该对象的 this
在 JavaScript 中,this 是“自由”的,它的值是在调用时计算出来的,它的值并不取决于方法声明的位置,而是取决于在“点符号前”的是什么对象。
在运行时对 this 求值的这个概念既有优点也有缺点。一方面,函数可以被重用于不同的对象。另一方面,更大的灵活性造成了更大的出错的可能。

箭头函数 没有自己的this

箭头函数有些特别:它们没有自己的 this。如果我们在这样的函数中引用 thisthis 值取决于外部“正常的”函数。
举个例子,这里的 arrow() 使用的 this 来自于外部的 user.sayHi() 方法:

let user = {
  firstName: "Ilya",
  sayHi() {
    let arrow = () => alert(this.firstName);
    arrow();
  }
};

user.sayHi(); // Ilya
// 和上面相同
let user = {
  firstName: "Ilya",
  sayHi() {
    alert(this.firstName);
  }
};

// 错误的结果
let user = {
  firstName: "Ilya",
  sayHi(): () => alert(this.firstName)
};

设置 this 的规则不考虑对象定义。只有调用那一刻才重要。

function makeUser() {
  return {
    name: "John",
    ref: this
  };
}

let user = makeUser();

alert( user.ref.name ); // 结果是什么?

Error: Cannot read property 'name' of...
以下产生相同的结果

function makeUser(){
  return this; // 这次这里没有对象字面量
}

alert( makeUser().name ); 
// Error: Cannot read property 'name' of...

以下产生正常的结果

function makeUser() {
  return {
    name: "John",
    ref() {
      return this;
    }
  };
}

let user = makeUser();

alert( user.ref().name ); // John

现在正常了,因为 user.ref() 是一个方法。this 的值为点符号 . 前的这个对象。

构造器和操作符 "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;(隐式返回)
}

构造器的主要目的 —— 实现可重用的对象创建代码。
任何函数(除了箭头函数,它没有自己的 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 调用的“常规模式”。

function User(name) {
  if (!new.target) { // 如果你没有通过 new 运行我
    return new User(name); // ……我会给你添加 new
  }

  this.name = name;
}

let john = User("John"); // 将调用重定向到新用户
alert(john.name); // John

这种方法有时被用在库中以使语法更加灵活。这样人们在调用函数时,无论是否使用了 new,程序都能工作。

构造器的 return

通常,构造器没有 return 语句。它们的任务是将所有必要的东西写入 this,并自动转换为结果。
但是,如果这有一个 return 语句,那么规则就简单了:

  • 如果 return 返回的是一个对象,则返回这个对象,而不是 this
  • 如果 return 返回的是一个原始类型,则忽略。
    换句话说,带有对象的 return 返回该对象,在所有其他情况下返回 this

通常构造器没有 return 语句。

省略括号
btw,如果没有参数,我们可以省略 new 后的括号:

let user = new User; // <-- 没有参数
// 等同于
let user = new User();

这里省略括号不被认为是一种“好风格”,但是规范允许使用该语法。

构造器中的方法

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() { ... }
}
*/

可选链 ?.

啊哈,这里是小尾巴~
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇