web前端必备的 JavaScript 基础知识梳理总结

  1. JavaScript简介
    JavaScript 最开始是专门为浏览器设计的一门语言,但是现在也被用于很多其他的环境。

如今,JavaScript 已经成为了与 HTML/CSS 完全集成的,使用最广泛的浏览器语言。

有很多其他的语言可以被“编译”成 JavaScript,这些语言还提供了更多的功能。建议最好了解一下这些语言,至少在掌握了 JavaScript 之后大致的了解一下。

  1. 变量
    我们可以使用 var、let 或 const 声明变量来存储数据。

let — 现代的变量声明方式。

var — 老旧的变量声明方式。一般情况下,我们不会再使用它。但是,我们会在 旧时的 “var” 章节介绍 var 和 let 的微妙差别,以防你需要它们。

const — 类似于 let,但是变量的值无法被修改。

变量应当以一种容易理解变量内部是什么的方式进行命名。

  1. 数据类型
    JavaScript 中有八种基本的数据类型(译注:前七种为基本数据类型,也称为原始类型,而 object 为复杂数据类型)。

number 用于任何类型的数字:整数或浮点数,在 ±(253-1) 范围内的整数。

bigint 用于任意长度的整数。

string 用于字符串:一个字符串可以包含 0 个或多个字符,所以没有单独的单字符类型。

boolean 用于 true 和 false。

null 用于未知的值 —— 只有一个 null 值的独立类型。

undefined 用于未定义的值 —— 只有一个 undefined 值的独立类型。

symbol 用于唯一的标识符。

object 用于更复杂的数据结构。

我们可以通过 typeof 运算符查看存储在变量中的数据类型。

两种形式:typeof x 或者 typeof(x)。

以字符串的形式返回类型名称,例如 “string”。

typeof null 会返回 “object” —— 这是 JavaScript 编程语言的一个错误,实际上它并不是一个 object。

  1. 类型转换
    有三种常用的类型转换:转换为 string 类型、转换为 number 类型和转换为 boolean 类型。

字符串转换 —— 转换发生在输出内容的时候,也可以通过 String(value) 进行显式转换。原始类型值的 string 类型转换通常是很明显的。

数字型转换 —— 转换发生在进行算术操作时,也可以通过 Number(value) 进行显式转换。

数字型转换遵循以下规则:

图片
布尔型转换遵循以下规则:布尔型转换 —— 转换发生在进行逻辑操作时,也可以通过 Boolean(value) 进行显式转换。

图片
对 undefined 进行数字型转换时,输出结果为 NaN,而非 0。上述的大多数规则都容易理解和记忆。人们通常会犯错误的值得注意的例子有以下几个:

对 “0” 和只有空格的字符串(比如:" ")进行布尔型转换时,输出结果为 true。
5. 值的比较
比较运算符始终返回布尔值。

字符串的比较,会按照“词典”顺序逐字符地比较大小。

当对不同类型的值进行比较时,它们会先被转化为数字(不包括严格相等检查)再进行比较。

在非严格相等 == 下,null 和 undefined 相等且各自不等于任何其他的值。

在使用 > 或 < 进行比较时,需要注意变量可能为 null/undefined 的情况。比较好的方法是单独检查变量是否等于 null/undefined。

  1. 空值合并运算符 ‘??’
    空值合并运算符 ?? 提供了一种从列表中选择第一个“已定义的”值的简便方式。
    它被用于为变量分配默认值:

// 当 height 的值为 null 或 undefined 时,将 height 的值设置为 100
height = height ?? 100;
复制代码
?? 运算符的优先级非常低,仅略高于 ? 和 =,因此在表达式中使用它时请考虑添加括号。

如果没有明确添加括号,不能将其与 || 或 && 一起使用。

  1. 循环:while 和 for
    我们学习了三种循环:

while —— 每次迭代之前都要检查条件。

do…while —— 每次迭代后都要检查条件。

for (;😉 —— 每次迭代之前都要检查条件,可以使用其他设置。

通常使用 while(true) 来构造“无限”循环。这样的循环和其他循环一样,都可以通过 break 指令来终止。

如果我们不想在当前迭代中做任何事,并且想要转移至下一次迭代,那么可以使用 continue 指令。

break/continue 支持循环前的标签。标签是 break/continue 跳出嵌套循环以转到外部的唯一方法。

  1. 函数
    函数声明方式如下所示:

function name(parameters, delimited, by, comma) {
/* code */
}
复制代码
作为参数传递给函数的值,会被复制到函数的局部变量。

函数可以访问外部变量。但它只能从内到外起作用。函数外部的代码看不到函数内的局部变量。

函数可以返回值。如果没有返回值,则其返回的结果是 undefined。

为了使代码简洁易懂,建议在函数中主要使用局部变量和参数,而不是外部变量。

与不获取参数但将修改外部变量作为副作用的函数相比,获取参数、使用参数并返回结果的函数更容易理解。

函数命名:

函数名应该清楚地描述函数的功能。当我们在代码中看到一个函数调用时,一个好的函数名能够让我们马上知道这个函数的功能是什么,会返回什么。

一个函数是一个行为,所以函数名通常是动词。

目前有许多优秀的函数名前缀,如 create…、show…、get…、check… 等等。使用它们来提示函数的作用吧。

  1. 函数表达式
    函数是值。它们可以在代码的任何地方被分配,复制或声明。

如果函数在主代码流中被声明为单独的语句,则称为“函数声明”。

如果该函数是作为表达式的一部分创建的,则称其“函数表达式”。

在执行代码块之前,内部算法会先处理函数声明。所以函数声明在其被声明的代码块内的任何位置都是可见的。

函数表达式在执行流程到达时创建。

在大多数情况下,当我们需要声明一个函数时,最好使用函数声明,因为函数在被声明之前也是可见的。这使我们在代码组织方面更具灵活性,通常也会使得代码可读性更高。

所以,仅当函数声明不适合对应的任务时,才应使用函数表达式。

  1. 箭头函数,基础知识
    对于一行代码的函数来说,箭头函数是相当方便的。它具体有两种:

不带花括号:(…args) => expression — 右侧是一个表达式:函数计算表达式并返回其结果。

带花括号:(…args) => { body } — 花括号允许我们在函数中编写多个语句,但是我们需要显式地 return 来返回一些内容。

  1. 对象
    对象是具有一些特殊特性的关联数组。

它们存储属性(键值对),其中:

属性的键必须是字符串或者 symbol(通常是字符串)。

值可以是任何类型。

我们可以用下面的方法访问属性:

点符号: obj.property。

方括号 obj[“property”],方括号允许从变量中获取键,例如 obj[varWithKey]。

其他操作:

删除属性:delete obj.prop。

检查是否存在给定键的属性:“key” in obj。

遍历对象:for(let key in obj) 循环。

我们在这一章学习的叫做“普通对象(plain object)”,或者就叫对象。

JavaScript 中还有很多其他类型的对象:

Array 用于存储有序数据集合,

Date 用于存储时间日期,

Error 用于存储错误信息。

……等等。

它们有着各自特别的特性,我们将在后面学习到。有时候大家会说“Array 类型”或“Date 类型”,但其实它们并不是自身所属的类型,而是属于一个对象类型即 “object”。它们以不同的方式对 “object” 做了一些扩展。

  1. 对象引用和复制
    对象通过引用被赋值和拷贝。换句话说,一个变量存储的不是“对象的值”,而是一个对值的“引用”(内存地址)。因此,拷贝此类变量或将其作为函数参数传递时,所拷贝的是引用,而不是对象本身。

所有通过被拷贝的引用的操作(如添加、删除属性)都作用在同一个对象上。

为了创建“真正的拷贝”(一个克隆),我们可以使用 Object.assign 来做所谓的“浅拷贝”(嵌套对象被通过引用进行拷贝)或者使用“深拷贝”函数,例如 _.cloneDeep(obj)[1]。

  1. 对象方法,“this”
    存储在对象属性中的函数被称为“方法”。

方法允许对象进行像 object.doSomething() 这样的“操作”。

方法可以将对象引用为 this。

this 的值是在程序运行时得到的。

一个函数在声明时,可能就使用了 this,但是这个 this 只有在函数被调用时才会有值。

可以在对象之间复制函数。

以“方法”的语法调用函数时:object.method(),调用过程中的 this 值是 object。

请注意箭头函数有些特别:它们没有 this。在箭头函数内部访问到的 this 都是从外部获取的。

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

obj?.prop —— 如果 obj 存在则返回 obj.prop,否则返回 undefined。

obj?.[prop] —— 如果 obj 存在则返回 obj[prop],否则返回 undefined。

obj.method?.() —— 如果 obj.method 存在则调用 obj.method(),否则返回 undefined。

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

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

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

  1. Symbol 类型
    Symbol 是唯一标识符的基本类型

Symbol 是使用带有可选描述(name)的 Symbol() 调用创建的。

Symbol 总是不同的值,即使它们有相同的名字。如果我们希望同名的 Symbol 相等,那么我们应该使用全局注册表:Symbol.for(key) 返回(如果需要的话则创建)一个以 key 作为名字的全局 Symbol。使用 Symbol.for 多次调用 key 相同的 Symbol 时,返回的就是同一个 Symbol。

Symbol 有两个主要的使用场景:

“隐藏” 对象属性。如果我们想要向“属于”另一个脚本或者库的对象添加一个属性,我们可以创建一个 Symbol 并使用它作为属性的键。Symbol 属性不会出现在 for…in 中,因此它不会意外地被与其他属性一起处理。并且,它不会被直接访问,因为另一个脚本没有我们的 symbol。因此,该属性将受到保护,防止被意外使用或重写。
因此我们可以使用 Symbol 属性“秘密地”将一些东西隐藏到我们需要的对象中,但其他地方看不到它。

JavaScript 使用了许多系统 Symbol,这些 Symbol 可以作为 Symbol.* 访问。我们可以使用它们来改变一些内置行为。例如,在本教程的后面部分,我们将使用 Symbol.iterator 来进行 迭代[2] 操作,使用 Symbol.toPrimitive 来设置 对象原始值的转换[3] 等等。
从技术上说,Symbol 不是 100% 隐藏的。有一个内置方法 Object.getOwnPropertySymbols(obj)[4] 允许我们获取所有的 Symbol。还有一个名为 Reflect.ownKeys(obj)[5] 的方法可以返回一个对象的 所有 键,包括 Symbol。所以它们并不是真正的隐藏。但是大多数库、内置方法和语法结构都没有使用这些方法。

  1. 数字类型
    要写有很多零的数字:

将 “e” 和 0 的数量附加到数字后。就像:123e6 与 123 后面接 6 个 0 相同。

“e” 后面的负数将使数字除以 1 后面接着给定数量的零的数字。例如 123e-6 表示 0.000123(123 的百万分之一)。

对于不同的数字系统:

可以直接在十六进制(0x),八进制(0o)和二进制(0b)系统中写入数字。

parseInt(str,base) 将字符串 str 解析为在给定的 base 数字系统中的整数,2 ≤ base ≤ 36。

num.toString(base) 将数字转换为在给定的 base 数字系统中的字符串。

要将 12pt 和 100px 之类的值转换为数字:

使用 parseInt/parseFloat 进行“软”转换,它从字符串中读取数字,然后返回在发生 error 前可以读取到的值。
小数:

使用 Math.floor,Math.ceil,Math.trunc,Math.round 或 num.toFixed(precision) 进行舍入。

请确保记住使用小数时会损失精度。

更多数学函数:

需要时请查看 Math[6] 对象。这个库很小,但是可以满足基本的需求。
17. 字符串
有 3 种类型的引号。反引号允许字符串跨越多行并可以使用 ${…} 在字符串中嵌入表达式。

JavaScript 中的字符串使用的是 UTF-16 编码。

我们可以使用像 \n 这样的特殊字符或通过使用 \u… 来操作它们的 unicode 进行字符插入。

获取字符时,使用 []。

获取子字符串,使用 slice 或 substring。

字符串的大/小写转换,使用:toLowerCase/toUpperCase。

查找子字符串时,使用 indexOf 或 includes/startsWith/endsWith 进行简单检查。

根据语言比较字符串时使用 localeCompare,否则将按字符代码进行比较。

还有其他几种有用的字符串方法:

str.trim() —— 删除字符串前后的空格 (“trims”)。

str.repeat(n) —— 重复字符串 n 次。

……更多内容细节请参见 手册[7]。

  1. 数组
    数组是一种特殊的对象,适用于存储和管理有序的数据项。

声明:

// 方括号 (常见用法) let arr = [item1, item2…];

// new Array (极其少见) let arr = new Array(item1, item2…);

调用 new Array(number) 会创建一个给定长度的数组,但不含有任何项。

length 属性是数组的长度,准确地说,它是数组最后一个数字索引值加一。它由数组方法自动调整。

如果我们手动缩短 length,那么数组就会被截断。

我们可以通过下列操作以双端队列的方式使用数组:

push(…items) 在末端添加 items 项。

pop() 从末端移除并返回该元素。

shift() 从首端移除并返回该元素。

unshift(…items) 从首端添加 items 项。

遍历数组的元素:

for (let i=0; i<arr.length; i++) — 运行得最快,可兼容旧版本浏览器。

for (let item of arr) — 现代语法,只能访问 items。

for (let i in arr) — 永远不要用这个。

比较数组时,不要使用 == 运算符(当然也不要使用 > 和 < 等运算符),因为它们不会对数组进行特殊处理。它们通常会像处理任意对象那样处理数组,这通常不是我们想要的。

但是,我们可以使用 for…of 循环来逐项比较数组。

  1. 数组方法
    数组方法备忘单:

添加/删除元素:

push(…items) —— 向尾端添加元素,

pop() —— 从尾端提取一个元素,

shift() —— 从首端提取一个元素,

unshift(…items) —— 向首端添加元素,

splice(pos, deleteCount, …items) —— 从 pos 开始删除 deleteCount 个元素,并插入 items。

slice(start, end) —— 创建一个新数组,将从索引 start 到索引 end(但不包括 end)的元素复制进去。

concat(…items) —— 返回一个新数组:复制当前数组的所有元素,并向其中添加 items。如果 items 中的任意一项是一个数组,那么就取其元素。

搜索元素:

indexOf/lastIndexOf(item, pos) —— 从索引 pos 开始搜索 item,搜索到则返回该项的索引,否则返回 -1。

includes(value) —— 如果数组有 value,则返回 true,否则返回 false。

find/filter(func) —— 通过 func 过滤元素,返回使 func 返回 true 的第一个值/所有值。

findIndex 和 find 类似,但返回索引而不是值。

遍历元素:

forEach(func) —— 对每个元素都调用 func,不返回任何内容。
转换数组:

map(func) —— 根据对每个元素调用 func 的结果创建一个新数组。

sort(func) —— 对数组进行原位(in-place)排序,然后返回它。

reverse() —— 原位(in-place)反转数组,然后返回它。

split/join —— 将字符串转换为数组并返回。

reduce/reduceRight(func, initial) —— 通过对每个元素调用 func 计算数组上的单个值,并在调用之间传递中间结果。

其他:

Array.isArray(arr) 检查 arr 是否是一个数组。
请注意,sort,reverse 和 splice 方法修改的是数组本身。

这些是最常用的方法,它们覆盖 99% 的用例。但是还有其他几个:

arr.some(fn)[8]/arr.every(fn)[9] 检查数组。
与 map 类似,对数组的每个元素调用函数 fn。如果任何/所有结果为 true,则返回 true,否则返回 false。

这两个方法的行为类似于 || 和 && 运算符:如果 fn 返回一个真值,arr.some() 立即返回 true 并停止迭代其余数组项;如果 fn 返回一个假值,arr.every() 立即返回 false 并停止对其余数组项的迭代。

我们可以使用 every 来比较数组:

function arraysEqual(arr1, arr2) {
return arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]);
}

alert( arraysEqual([1, 2], [1, 2])); // true
复制代码
arr.fill(value, start, end)[10] —— 从索引 start 到 end,用重复的 value 填充数组。

arr.copyWithin(target, start, end)[11] —— 将从位置 start 到 end 的所有元素复制到 自身 的 target 位置(覆盖现有元素)。

arr.flat(depth)[12]/arr.flatMap(fn)[13] 从多维数组创建一个新的扁平数组。

Array.of(element0[, element1[, …[, elementN]]])[14] 基于可变数量的参数创建一个新的 Array 实例,而不需要考虑参数的数量或类型。

有关完整列表,请参阅 手册[15]。

  1. Iterable object(可迭代对象)
    可以应用 for…of 的对象被称为 可迭代的。

技术上来说,可迭代对象必须实现 Symbol.iterator 方法。

objSymbol.iterator 的结果被称为 迭代器(iterator)。由它处理进一步的迭代过程。

一个迭代器必须有 next() 方法,它返回一个 {done: Boolean, value: any} 对象,这里 done:true 表明迭代结束,否则 value 就是下一个值。

Symbol.iterator 方法会被 for…of 自动调用,但我们也可以直接调用它。

内置的可迭代对象例如字符串和数组,都实现了 Symbol.iterator。

字符串迭代器能够识别代理对(surrogate pair)。(译注:代理对也就是 UTF-16 扩展字符。)

有索引属性和 length 属性的对象被称为 类数组对象。这种对象可能还具有其他属性和方法,但是没有数组的内建方法。

如果我们仔细研究一下规范 —— 就会发现大多数内建方法都假设它们需要处理的是可迭代对象或者类数组对象,而不是“真正的”数组,因为这样抽象度更高。

Array.from(obj[, mapFn, thisArg]) 将可迭代对象或类数组对象 obj 转化为真正的数组 Array,然后我们就可以对它应用数组的方法。可选参数 mapFn 和 thisArg 允许我们将函数应用到每个元素。

  1. Map and Set(映射和集合)
    Map —— 是一个带键的数据项的集合。

方法和属性如下:

new Map([iterable]) —— 创建 map,可选择带有 [key,value] 对的 iterable(例如数组)来进行初始化。

map.set(key, value) —— 根据键存储值。

map.get(key) —— 根据键来返回值,如果 map 中不存在对应的 key,则返回 undefined。

map.has(key) —— 如果 key 存在则返回 true,否则返回 false。

map.delete(key) —— 删除指定键的值。

map.clear() —— 清空 map 。

map.size —— 返回当前元素个数。

与普通对象 Object 的不同点:

任何键、对象都可以作为键。

有其他的便捷方法,如 size 属性。

Set —— 是一组唯一值的集合。

方法和属性:

new Set([iterable]) —— 创建 set,可选择带有 iterable(例如数组)来进行初始化。

set.add(value) —— 添加一个值(如果 value 存在则不做任何修改),返回 set 本身。

set.delete(value) —— 删除值,如果 value 在这个方法调用的时候存在则返回 true ,否则返回 false。

set.has(value) —— 如果 value 在 set 中,返回 true,否则返回 false。

set.clear() —— 清空 set。

set.size —— 元素的个数。

在 Map 和 Set 中迭代总是按照值插入的顺序进行的,所以我们不能说这些集合是无序的,但是我们不能对元素进行重新排序,也不能直接按其编号来获取元素。

  1. WeakMap and WeakSet(弱映射和弱集合)
    WeakMap 是类似于 Map 的集合,它仅允许对象作为键,并且一旦通过其他方式无法访问它们,便会将它们与其关联值一同删除。

WeakSet 是类似于 Set 的集合,它仅存储对象,并且一旦通过其他方式无法访问它们,便会将其删除。

它们都不支持引用所有键或其计数的方法和属性。仅允许单个操作。

WeakMap 和 WeakSet 被用作“主要”对象存储之外的“辅助”数据结构。一旦将对象从主存储器中删除,如果该对象仅被用作 WeakMap 或 WeakSet 的键,那么它将被自动清除。

  1. 解构赋值
    解构赋值可以立即将一个对象或数组映射到多个变量上。

解构对象的完整语法:

let {prop : varName = default, …rest} = object

这表示属性 prop 会被赋值给变量 varName,如果没有这个属性的话,就会使用默认值 default。

没有对应映射的对象属性会被复制到 rest 对象。

解构数组的完整语法:

let [item1 = default, item2, …rest] = array

数组的第一个元素被赋值给 item1,第二个元素被赋值给 item2,剩下的所有元素被复制到另一个数组 rest。

从嵌套数组/对象中提取数据也是可以的,此时等号左侧必须和等号右侧有相同的结构。
24. 日期和时间
在 JavaScript 中,日期和时间使用 Date[16] 对象来表示。我们不能只创建日期,或者只创建时间,Date 对象总是同时创建两者。

月份从 0 开始计数(对,一月是 0)。

一周中的某一天 getDay() 同样从 0 开始计算(0 代表星期日)。

当设置了超出范围的组件时,Date 会进行自我校准。这一点对于日/月/小时的加减很有用。

日期可以相减,得到的是以毫秒表示的两者的差值。因为当 Date 被转换为数字时,Date 对象会被转换为时间戳。

使用 Date.now() 可以更快地获取当前时间的时间戳。

和其他系统不同,JavaScript 中时间戳以毫秒为单位,而不是秒。

有时我们需要更加精准的时间度量。JavaScript 自身并没有测量微秒的方法(百万分之一秒),但大多数运行环境会提供。例如:浏览器有 performance.now()[17] 方法来给出从页面加载开始的以毫秒为单位的微秒数(精确到毫秒的小数点后三位):

alert(Loading started ${performance.now()}ms ago);
// 类似于 “Loading started 34731.26000000001ms ago”
// .26 表示的是微秒(260 微秒)
// 小数点后超过 3 位的数字是精度错误,只有前三位数字是正确的
复制代码
Node.js 有 microtime 模块以及其他方法。从技术上讲,几乎所有的设备和环境都允许获取更高精度的数值,只是不是通过 Date 对象。

  1. JSON 方法,toJSON
    JSON 是一种数据格式,具有自己的独立标准和大多数编程语言的库。

JSON 支持 object,array,string,number,boolean 和 null。

JavaScript 提供序列化(serialize)成 JSON 的方法 JSON.stringify[18] 和解析 JSON 的方法 JSON.parse[19]。

这两种方法都支持用于智能读/写的转换函数。

如果一个对象具有 toJSON,那么它会被 JSON.stringify 调用。

  1. 递归和堆栈
    术语:

递归 是编程的一个术语,表示从自身调用函数(译注:也就是自调用)。递归函数可用于以更优雅的方式解决问题。
当一个函数调用自身时,我们称其为 递归步骤。递归的 基础 是函数参数使任务简单到该函数不再需要进行进一步调用。

递归定义[20] 的数据结构是指可以使用自身来定义的数据结构。
例如,链表可以被定义为由对象引用一个列表(或 null)而组成的数据结构。

list = { value, next -> list }
复制代码
像 HTML 元素树或者本章中的 department 树等,本质上也是递归:它们有分支,而且分支又可以有其他分支。

就像我们在示例 sumSalary 中看到的那样,可以使用递归函数来遍历它们。

任何递归函数都可以被重写为迭代(译注:也就是循环)形式。有时这是在优化代码时需要做的。但对于大多数任务来说,递归方法足够快,并且容易编写和维护。

  1. Rest 参数与 Spread 语法
    当我们在代码中看到 “…” 时,它要么是 rest 参数,要么就是 spread 语法。

有一个简单的方法可以区分它们:

若 … 出现在函数参数列表的最后,那么它就是 rest 参数,它会把参数列表中剩余的参数收集到一个数组中。

若 … 出现在函数调用或类似的表达式中,那它就是 spread 语法,它会把一个数组展开为列表。

使用场景:

Rest 参数用于创建可接受任意数量参数的函数。

Spread 语法用于将数组传递给通常需要含有许多参数的列表的函数。

它们俩的出现帮助我们轻松地在列表和参数数组之间来回转换。

“旧式”的 arguments(类数组且可迭代的对象)也依然能够帮助我们获取函数调用中的所有参数。

  1. 全局对象
    全局对象包含应该在任何位置都可见的变量。

其中包括 JavaScript 的内建方法,例如 “Array” 和环境特定(environment-specific)的值,例如 window.innerHeight — 浏览器中的窗口高度。

全局对象有一个通用名称 globalThis。

……但是更常见的是使用“老式”的环境特定(environment-specific)的名字,例如 window(浏览器)和 global(Node.js)。

仅当值对于我们的项目而言确实是全局的时,才应将其存储在全局对象中。并保持其数量最少。

在浏览器中,除非我们使用 modules[21],否则使用 var 声明的全局函数和变量会成为全局对象的属性。

为了使我们的代码面向未来并更易于理解,我们应该使用直接的方式访问全局对象的属性,如 window.x。

  1. 函数对象,NFE
    函数就是对象。

我们介绍了它们的一些属性:

name —— 函数的名字。通常取自函数定义,但如果函数定义时没设定函数名,JavaScript 会尝试通过函数的上下文猜一个函数名(例如把赋值的变量名取为函数名)。

length —— 函数定义时的入参的个数。Rest 参数不参与计数。

如果函数是通过函数表达式的形式被声明的(不是在主代码流里),并且附带了名字,那么它被称为命名函数表达式(Named Function Expression)。这个名字可以用于在该函数内部进行自调用,例如递归调用等。

此外,函数可以带有额外的属性。很多知名的 JavaScript 库都充分利用了这个功能。

它们创建一个“主”函数,然后给它附加很多其它“辅助”函数。例如,jQuery[22] 库创建了一个名为 $ 的函数。lodash[23] 库创建一个 _ 函数,然后为其添加了 .add、.keyBy 以及其它属性(想要了解更多内容,参查阅 docs[24])。实际上,它们这么做是为了减少对全局空间的污染,这样一个库就只会有一个全局变量。这样就降低了命名冲突的可能性。

所以,一个函数本身可以完成一项有用的工作,还可以在自身的属性中附带许多其他功能。

  1. “new Function” 语法
    语法:

let func = new Function ([arg1, arg2, …argN], functionBody);
复制代码
由于历史原因,参数也可以按逗号分隔符的形式给出。

以下三种声明的含义相同:

new Function(‘a’, ‘b’, ‘return a + b’); // 基础语法
new Function(‘a,b’, ‘return a + b’); // 逗号分隔
new Function(‘a , b’, ‘return a + b’); // 逗号和空格分隔
复制代码
使用 new Function 创建的函数,它的 [[Environment]] 指向全局词法环境,而不是函数所在的外部词法环境。因此,我们不能在 new Function 中直接使用外部变量。不过这样是好事,这有助于降低我们代码出错的可能。并且,从代码架构上讲,显式地使用参数传值是一种更好的方法,并且避免了与使用压缩程序而产生冲突的问题。

  1. 调度:setTimeout 和 setInterval
    setTimeout(func, delay, …args) 和 setInterval(func, delay, …args) 方法允许我们在 delay 毫秒之后运行 func 一次或以 delay 毫秒为时间间隔周期性运行 func。

要取消函数的执行,我们应该调用 clearInterval/clearTimeout,并将 setInterval/setTimeout 返回的值作为入参传入。

嵌套的 setTimeout 比 setInterval 用起来更加灵活,允许我们更精确地设置两次执行之间的时间。

零延时调度 setTimeout(func, 0)(与 setTimeout(func) 相同)用来调度需要尽快执行的调用,但是会在当前脚本执行完成后进行调用。

浏览器会将 setTimeout 或 setInterval 的五层或更多层嵌套调用(调用五次之后)的最小延时限制在 4ms。这是历史遗留问题。

请注意,所有的调度方法都不能 保证 确切的延时。

例如,浏览器内的计时器可能由于许多原因而变慢:

CPU 过载。

浏览器页签处于后台模式。

笔记本电脑用的是电池供电(译注:使用电池供电会以降低性能为代价提升续航)。

所有这些因素,可能会将定时器的最小计时器分辨率(最小延迟)增加到 300ms 甚至 1000ms,具体以浏览器及其设置为准。

  1. 装饰器模式和转发,call/apply
    装饰器 是一个围绕改变函数行为的包装器。主要工作仍由该函数来完成。

装饰器可以被看作是可以添加到函数的 “features” 或 “aspects”。我们可以添加一个或添加多个。而这一切都无需更改其代码!

为了实现 cachingDecorator,我们研究了以下方法:

func.call(context, arg1, arg2…)[25] —— 用给定的上下文和参数调用 func。

func.apply(context, args)[26] —— 调用 func 将 context 作为 this 和类数组的 args 传递给参数列表。

通用的 呼叫转移(call forwarding) 通常是使用 apply 完成的:

let wrapper = function() {
return original.apply(this, arguments);
};
复制代码
我们也可以看到一个 方法借用(method borrowing) 的例子,就是我们从一个对象中获取一个方法,并在另一个对象的上下文中“调用”它。采用数组方法并将它们应用于参数 arguments 是很常见的。另一种方法是使用 Rest 参数对象,该对象是一个真正的数组。

  1. 函数绑定
    方法 func.bind(context, …args) 返回函数 func 的“绑定的(bound)变体”,它绑定了上下文 this 和第一个参数(如果给定了)。

通常我们应用 bind 来绑定对象方法的 this,这样我们就可以把它们传递到其他地方使用。例如,传递给 setTimeout。

当我们绑定一个现有的函数的某些参数时,绑定后的(不太通用的)函数被称为 partially applied 或 partial。

当我们不想一遍又一遍地重复相同的参数时,partial 非常有用。就像我们有一个 send(from, to) 函数,并且对于我们的任务来说,from 应该总是一样的,那么我们就可以搞一个 partial 并使用它。

  1. 深入理解箭头函数
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值