JavaScript学习笔记

116 阅读21分钟

现代JavaScript教程学习笔记 创建时间: March 27, 2022 11:55 AM 标签: 前端 类别: 笔记 # 类型转换 - number — 可以是浮点数,也可以是整数, - bigint — 用于任意长度的整数, - string — 字符串类型, - boolean — 逻辑值:true/false, - null — 具有单个值 null 的类型,表示“空”或“不存在”, - undefined — 具有单个值 undefined 的类型,表示“未分配(未定义)”, - object 和 symbol — 对于复杂的数据结构和唯一标识符。 ## 字符串转换 jsx String(value); ## 数字型转换 jsx Number(value) | 值 | 变成…… | | --- | --- | | undefined | NaN | | null | 0 | | true/false | 1/0 | | string | “按原样读取”字符串,两端的空白会被忽略。空字符串变成0。转换出错则变成NaN。 | ## 布尔类型转换 jsx Boolean(value) | 值 | 变成…… | | --- | --- | | 0,null, undefined, NaN,”” | false | | 其它值 | true | # 基础运算符,数学 ## 数字转化,一元运算符 + 一元运算符加号,或者说,加号 +  应用于单个值,对数字没有任何作用。但是如果运算元不是数字,加号 +  则会将其转化为数字。 例如: jsx // 对数字无效 let x = 1; alert( +x ); // 1 let y = -2; alert( +y ); // -2 // 转化非数字 alert( +true ); // 1 alert( +"" ); // 0 ## 自增/自减 - 当运算符置于变量后,被称为“后置形式”:counter++。 - 当运算符置于变量前,被称为“前置形式”:++counter 前置形式返回一个新的值,但后置返回原来的值(做加法/减法之前的值)。 # 值的比较 ## 不同类型间的比较 当对不同类型的值进行比较时,JavaScript 会首先将其转化为数字(number)再判定大小。 ## 严格相等(===)与非严格相等(==) 在非严格相等 == 下,null 和 undefined 相等且各自不等于任何其他的值。 严格相等运算符 === 在进行比较时不会做任何的类型转换。 # 逻辑运算符 如果操作数不是布尔值,那么它将会被转化为布尔值来参与运算。 ## 或(||) 或运算寻找第一个真值 - 从左到右依次计算操作数。 - 处理每一个操作数时,都将其转化为布尔值。如果结果是 true,就停止计算,返回这个操作数的初始值。 - 如果所有的操作数都被计算过(也就是,转换结果都是 false),则返回最后一个操作数。 ## 与(&&) 与运算寻找第一个假值 - 从左到右依次计算操作数。 - 在处理每一个操作数时,都将其转化为布尔值。如果结果是 false,就停止计算,并返回这个操作数的初始值。 - 如果所有的操作数都被计算过(例如都是真值),则返回最后一个操作数。 > 与运算 &&  的优先级比或运算 ||  要高。 > > 非运算符 !  的优先级在所有逻辑运算符里面最高,所以它总是在 &&  和 ||  之前执行。 > ## 空值合并运算符(??) 如果第一个参数不是 null/undefined ,则 ??  返回第一个参数。否则,返回第二个参数。 - || 返回第一个  值。 - ?? 返回第一个 已定义的 值。 jsx let height = 0; alert(height || 100); // 100 alert(height ?? 100); // 0 # 函数 - 作为参数传递给函数的值,会被复制到函数的局部变量。 - 函数可以访问外部变量。但它只能从内到外起作用。函数外部的代码看不到函数内的局部变量。 - 函数可以返回值。如果没有返回值,则其返回的结果是 undefined。 ## 默认值 可以使用 = 为函数声明中的参数指定所谓的“默认值”(如果对应参数的值未被传递则使用) jsx function showMessage(from, text = "no text given") { alert( from + ": " + text ); } showMessage("Ann"); // Ann: no text given # 对象 对象是具有一些特殊特性的关联数组。 它们存储属性(键值对),其中: - 属性的键必须是字符串或者 symbol(通常是字符串)。其他类型会被自动地转换为字符串。例如,当数字 0 被用作对象的属性的键时,会被转换为字符串 "0" - 值可以是任何类型。 ## 判断属性是否存在 jsx // 1. 属性是否等于undefined let user = {} user.noSuchProperty === undefined // 2. in操作符 "key" in object 属性存在,但存储的值是 undefined 的时候需要用”in”操作符 ## “for…in” 循环 jsx for (let key in object) { // 对此对象属性中的每个键执行的代码 } ## 对象属性排序 整数属性会被进行排序,其他属性则按照创建的顺序显示。 > “整数属性”指的是一个可以在不做任何更改的情况下与一个整数进行相互转换的字符串 > ## 对象的引用和复制 对象通过引用被赋值和拷贝。换句话说,一个变量存储的不是“对象的值”,而是一个对值的“引用”(内存地址)。因此,拷贝此类变量或将其作为函数参数传递时,所拷贝的是引用,而不是对象本身。 ### Objects.assign(浅拷贝) 1. 合并多个对象 2. 简单拷贝 jsx Object.assign(dest, [src1, src2, src3...]) - 第一个参数 dest 是指目标对象。 - 更后面的参数 src1, ..., srcN(可按需传递多个参数)是源对象。 - 该方法将所有源对象的属性拷贝到目标对象 dest 中。换句话说,从第二个开始的所有参数的属性都被拷贝到第一个参数的对象中。 - 调用结果返回 dest。 ### 深拷贝 可以使用lodash库的_.cloneDeep(obj) ## 对象方法,"this" 存储在对象属性中的函数被称为“方法”。 ### 方法中的 “this” this的值就是在点之前的这个对象,即调用该方法的对象。 JavaScript 中的 this可以用于任何函数,即使它不是对象的方法。 this的值是在代码运行时计算出来的,它取决于代码上下文。 jsx let user = { name: "John" }; let admin = { name: "Admin" }; function sayHi() { alert( this.name ); } // 在两个对象中使用相同的函数 user.f = sayHi; admin.f = sayHi; // 这两个调用有不同的 this 值 // 函数内部的 "this" 是“点符号前面”的那个对象 user.f(); // John(this == user) admin.f(); // Admin(this == admin) sayHi() // this为undefined,因为sayHi没有作为对象的方法被调用 ### 箭头函数没有自己的 “this” 箭头函数有些特别:它们没有自己的 this。如果我们在这样的函数中引用 thisthis值取决于外部“正常的”函数。 ## 可选链 可选链 ?. 语法有三种形式: 1. obj?.prop —— 如果 obj 存在则返回 obj.prop,否则返回 undefined。 2. obj?.[prop] —— 如果 obj 存在则返回 obj[prop],否则返回 undefined。 3. obj.method?.() —— 如果 obj.method 存在则调用 obj.method(),否则返回 undefined。 ## Symbol类型 Symbol是唯一标识符的基本类型 Symbol 总是不同的值,即使它们有相同的名字。如果我们希望同名的 Symbol 相等,那么我们应该使用全局注册表:Symbol.for(key)返回(如果需要的话则创建)一个以 key 作为名字的全局 Symbol。使用 Symbol.for 多次调用 key相同的 Symbol 时,返回的就是同一个 Symbol。 Symbol在for...in中会被跳过,但Object.assign会同时复制字符串和 symbol 属性 ## 原始值转换 参考: zh.javascript.info/object-topr… # 数据类型 ## 数字类型 Infintiy/-Infinity/NaN属于 number  类型,但不是“普通”数字 jsx let billion = 1000000000; let billion = 1_000_000_000; let billion = 1e9; let mcs = 0.000001; // -6 除以 1 后面跟着 6 个 0 的数字 let mcs = 1e-6; 这里的下划线 _扮演了“语法糖”的角色,使得数字具有更强的可读性。JavaScript 引擎会直接忽略数字之间的 _,所以 上面两个例子其实是一样的。 ### toString(base) 方法 num.toString(base)返回在给定 base 进制数字系统中 num的字符串表示形式。 base的范围可以从 2 到 36。默认情况下是 10。 ### 舍入 - Math.floor 向下舍入 - Math.ceil 向上舍入 - Math.round 向最近的整数舍入 - Math.trunc 移除小数点后的所有内容而没有舍入 - toFixed(n) 将数字舍入到小数点后 n  位,并以字符串形式返回结果。 ### 测试:isFinite 和 isNaN 值 “NaN” 是独一无二的,它不等于任何东西,包括它自身 isNaN(value) 将其参数转换为数字,然后测试它是否为 NaN isFinite(value)将其参数转换为数字,如果是常规数字,则返回 true ### parseInt 和 parseFloat 使用加号 +或 Number() 的数字转换是严格的。如果一个值不完全是一个数字,就会失败 parseInt和parseFloat它们可以从字符串中“读取”数字,直到无法读取为止。如果发生 error,则返回收集到的数字。 jsx alert( parseInt('100px') ); // 100 alert( parseFloat('12.5em') ); // 12.5 alert( parseInt('12.3') ); // 12,只有整数部分被返回了 alert( parseFloat('12.3.4') ); // 12.3,在第二个点出停止了读取 alert( parseInt('a123') ); // NaN,第一个符号停止了读取 ## 字符串 ### str.indexOf(substr, pos) 它从给定位置 pos 开始,在 str中查找 substr,如果没有找到,则返回 -1 ,否则返回匹配成功的位置。 ### str.includes(substr, pos) 根据 str中是否包含 substr来返回 true/false。 ### str.slice(start [, end]) 返回字符串从 start 到(但不包括)end的部分。 ### str.codePointAt(pos) 返回在 pos位置的字符代码(UTF-16编码中对应的数字代码) ## 数组 ### 作用于数组末端的方法 - pop 取出并返回数组的最后一个元素 - push 在数组末端添加元素 ### 作用于数组首端的方法 - shift 取出数组的第一个元素并返回它 - unshift 在数组的首端添加元素 ### 循环 - for - for.. of ### length length实际上不是数组里元素的个数,而是最大的数字索引值加一。 length是可写的,如果我们手动增加它,则不会发生任何有趣的事儿。但是如果我们减少它,数组就会被截断。该过程是不可逆的。所以,清空数组最简单的方法就是:arr.length = 0; ### arr.splice(start[, deleteCount, elem1, ..., elemN]) 从索引 start开始修改 arr:删除 deleteCount 个元素并在当前位置插入 elem1, ..., elemN。最后返回已被删除元素的数组。 我们可以将 deleteCount 设置为 0splice方法就能够插入元素而不用删除任何元素 ### arr.slice([start], [end]) 它会返回一个新数组,将所有从索引 start 到 end(不包括 end )的数组项复制到一个新的数组start 和 end 都可以是负数,在这种情况下,从末尾计算索引。 ### arr.concat(arg1, arg2...) 它接受任意数量的参数 —— 数组或值都可以。 结果是一个包含来自于 arr,然后是 arg1arg2 的元素的新数组。 如果参数 argN 是一个数组,那么其中的所有元素都会被复制。否则,将复制参数本身。 ### are.forEach jsx arr.forEach(function(item, index, array) { // ... do something with item }); ### 数组中搜索 - arr.includes(item, from) —— 从索引 from 开始搜索 item,如果找到则返回 true(译注:如果没找到,则返回 false)。 - arr.lastIndexOf(item, from) —— 和上面相同,只是从右向左搜索。 - arr.indexOf(item, from) 从索引 from 开始搜索 item,如果找到则返回索引,否则返回 1。 ### arr.find jsx let result = arr.find(function(item, index, array) { // 如果返回 true,则返回 item 并停止迭代 // 对于假值(falsy)的情况,则返回 undefined }); ### arr.filter 语法与 find 大致相同,但是 filter  返回的是所有匹配元素组成的数组 jsx let results = arr.filter(function(item, index, array) { // 如果 true item 被 push 到 results,迭代继续 // 如果什么都没找到,则返回空数组 }); ### arr.map 对数组的每个元素都调用函数,并返回结果数组 jsx let result = arr.map(function(item, index, array) { // 返回新值而不是当前元素 }) ### arr.sort 对数组进行 原位(in-place)排序,更改元素的顺序。(译注:原位是指在此数组内,而非生成一个新数组。) 元素默认情况下被按字符串进行排序 要使用我们自己的排序顺序,我们需要提供一个函数作为 arr.sort()的参数 jsx function compare(a, b) { if (a > b) return 1; // 如果第一个值比第二个值大 if (a == b) return 0; // 如果两个值相等 if (a < b) return -1; // 如果第一个值比第二个值小 } let arr = [ 1, 2, 15 ]; arr.sort(compareNumeric); alert(arr); // 1, 2, 15 ### arr.reverse 用于颠倒 arr中元素的顺序。 ### split 和 join arr.split(delim) 通过给定的分隔符 delim将字符串分割成一个数组。 arr.join(glue) 它会在它们之间创建一串由 glue粘合的 arr项 ### reduce/reduceRight jsx let value = arr.reduce(function(accumulator, item, index, array) { // ... }, [initial]); 该函数一个接一个地应用于所有数组元素,并将其结果“搬运(carry on)”到下一个调用。 参数: - accumulator —— 是上一个函数调用的结果,第一次等于 initial(如果提供了 initial 的话)。 - item —— 当前的数组元素。 - index —— 当前索引。 - arr —— 数组本身。 jsx let arr = [1, 2, 3, 4, 5]; let result = arr.reduce((sum, current) => sum + current, 0); alert(result); // 15 如果没有初始值,那么 reduce会将数组的第一个元素作为初始值,并从第二个元素开始迭代。 arr.reduceRight 和 arr.reduce方法的功能一样,只是遍历为从右到左。 ## Iterable object(可迭代对象) 可以应用 for..of的对象被称为 可迭代的。 - 技术上来说,可迭代对象必须实现 Symbol.iterator 方法。 - obj[Symbol.iterator]() 的结果被称为 迭代器(iterator)。由它处理进一步的迭代过程。 - 一个迭代器必须有 next() 方法,它返回一个 {done: Boolean, value: any} 对象,这里 done:true 表明迭代结束,否则 value 就是下一个值。 - Symbol.iterator 方法会被 for..of 自动调用,但我们也可以直接调用它。 - 内建的可迭代对象例如字符串和数组,都实现了 Symbol.iterator。 - 字符串迭代器能够识别代理对(surrogate pair)。(译注:代理对也就是 UTF-16 扩展字符。) jsx let range = { from: 1, to: 5 }; // 1. for..of 调用首先会调用这个: range[Symbol.iterator] = function() { // ……它返回迭代器对象(iterator object): // 2. 接下来,for..of 仅与下面的迭代器对象一起工作,要求它提供下一个值 return { current: this.from, last: this.to, // 3. next() 在 for..of 的每一轮循环迭代中被调用 next() { // 4. 它将会返回 {done:.., value :...} 格式的对象 if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; }; // 现在它可以运行了! for (let num of range) { alert(num); // 1, 然后是 2, 3, 4, 5 } ## Map Map —— 是一个带键的数据项的集合。 方法和属性如下: - new Map([iterable]) —— 创建 map,可选择带有 [key,value] 对的 iterable(例如数组)来进行初始化。 - map.set(key, value) —— 根据键存储值,返回 map 自身。 - map.get(key) —— 根据键来返回值,如果 map 中不存在对应的 key,则返回 undefined。 - map.has(key) —— 如果 key 存在则返回 true,否则返回 false。 - map.delete(key) —— 删除指定键对应的值,如果在调用时 key 存在,则返回 true,否则返回 false。 - map.clear() —— 清空 map 。 - map.size —— 返回当前元素个数。 与普通对象 Object 的不同点: - 任何键、对象都可以作为键。 - 有其他的便捷方法,如 size 属性。 如果要在 map 里使用循环,可以使用以下三个方法: - map.keys() —— 遍历并返回所有的键(returns an iterable for keys),返回可迭代对象,并非数组 - map.values() —— 遍历并返回所有的值(returns an iterable for values), - map.entries() —— 遍历并返回所有的实体(returns an iterable for entries)[key, value]for..of 在默认情况下使用的就是这个。 jsx let recipeMap = new Map([ ['cucumber', 500], ['tomatoes', 350], ['onion', 50] ]); // 遍历所有的键(vegetables) for (let vegetable of recipeMap.keys()) { alert(vegetable); // cucumber, tomatoes, onion } // 遍历所有的值(amounts) for (let amount of recipeMap.values()) { alert(amount); // 500, 350, 50 } // 遍历所有的实体 [key, value] for (let entry of recipeMap) { // 与 recipeMap.entries() 相同 alert(entry); // cucumber,500 (and so on) } // 对每个键值对 (key, value) 运行 forEach 函数 recipeMap.forEach( (value, key, map) => { alert(`${key}: ${value}`); // cucumber: 500 etc }); Object.entries:从对象创建 Map jsx let obj = { name: "John", age: 30 }; let map = new Map(Object.entries(obj)); alert( map.get('name') ); // John Object.fromEntries:从 Map 创建对象 jsx let prices = Object.fromEntries([ ['banana', 1], ['orange', 2], ['meat', 4] ]); // 现在 prices = { banana: 1, orange: 2, meat: 4 } alert(prices.orange); // 2 let map = new Map(); map.set('banana', 1); map.set('orange', 2); map.set('meat', 4); let obj = Object.fromEntries(map.entries()); // 创建一个普通对象(plain object)(*) // 完成了! // obj = { banana: 1, orange: 2, meat: 4 } alert(obj.orange); // 2 ## Set Set 是一个特殊的类型集合 —— “值的集合”(没有键),它的每一个值只能出现一次。 它的主要方法如下: - new Set(iterable) —— 创建一个 set,如果提供了一个 iterable 对象(通常是数组),将会从数组里面复制值到 set 中。 - set.add(value) —— 添加一个值,返回 set 本身 - set.delete(value) —— 删除值,如果 value 在这个方法调用的时候存在则返回 true ,否则返回 false。 - set.has(value) —— 如果 value 在 set 中,返回 true,否则返回 false。 - set.clear() —— 清空 set。 - set.size —— 返回元素个数。 重复使用同一个值用 set.add(value)并不会发生什么改变。这就是 Set里面的每一个值只出现一次的原因。 我们可以使用 for..of或 forEach来遍历 Set jsx let set = new Set(["oranges", "apples", "bananas"]); for (let value of set) alert(value); // 与 forEach 相同: set.forEach((value, valueAgain, set) => { alert(value); }); ## WeakMap and WeakSet(弱映射和弱集合) WeakMap 是类似于 Map 的集合,它仅允许对象作为键,并且一旦通过其他方式无法访问它们,便会将它们与其关联值一同删除。 WeakSet 是类似于 Set 的集合,它仅存储对象,并且一旦通过其他方式无法访问它们,便会将其删除。 它们的主要优点是它们对对象是弱引用,所以被它们引用的对象很容易地被垃圾收集器移除。 这是以不支持 clearsizekeysvalues 等作为代价换来的…… WeakMap 和 WeakSet 被用作“主要”对象存储之外的“辅助”数据结构。一旦将对象从主存储器中删除,如果该对象仅被用作 WeakMap 或 WeakSet 的键,那么它将被自动清除。 ## Object.keys,values,entries 对于普通对象,下列这些方法是可用的: - Object.entries(obj) —— 返回一个包含该对象所有 [key, value] 键值对的数组。 - Object.values(obj) —— 返回一个包含该对象所有的值的数组。 - Object.keys(obj) —— 返回一个包含该对象所有的键的数组。 Map, Set 的keys/values/entries方法返回值为可迭代对象,Object的keys/values/entries方法返回真正的数组 ### 对象转换 对象缺少数组存在的许多方法,例如 map和 filter 等。 如果我们想应用它们,那么我们可以使用 Object.entries,然后使用 Object.fromEntries对象缺少数组存在的许多方法,例如 map 和 filter 等。 jsx let prices = { banana: 1, orange: 2, meat: 4, }; let doublePrices = Object.fromEntries( // 将价格转换为数组,将每个键/值对映射为另一对 // 然后通过 fromEntries 再将结果转换为对象 Object.entries(prices).map(entry => [entry[0], entry[1] * 2]) ); alert(doublePrices.meat); // 8 ## 解构赋值 ### 数组解构 jsx // 我们有一个存放了名字和姓氏的数组 let arr = ["John", "Smith"] // 解构赋值 // sets firstName = arr[0] // and surname = arr[1] let [firstName, surname] = arr; alert(firstName); // John alert(surname); // Smith let [firstName, surname] = "John Smith".split(' '); alert(firstName); // John alert(surname); // Smith 等号右侧可以是任何可迭代对象 与. entries()方法进行循环操作 jsx let user = { name: "John", age: 30 }; // 循环遍历键—值对 for (let [key, value] of Object.entries(user)) { alert(`${key}:${value}`); // name:John, then age:30 } 用于 Map的类似的代码更简单,因为它是可迭代的: jsx let user = new Map(); user.set("name", "John"); user.set("age", "30"); // Map 是以 [key, value] 对的形式进行迭代的,非常便于解构 for (let [key, value] of user) { alert(`${key}:${value}`); // name:John, then age:30 } ### 其余的 ‘…’ 通常,如果数组比左边的列表长,那么“其余”的数组项会被省略。 jsx let [name1, name2] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; alert(name1); // Julius alert(name2); // Caesar // 其余数组项未被分配到任何地方 如果我们还想收集其余的数组项 —— 我们可以使用三个点 "..." 来再加一个参数以获取“其余”数组项: jsx let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; // rest 是包含从第三项开始的其余数组项的数组 alert(rest[0]); // Consul alert(rest[1]); // of the Roman Republic alert(rest.length); // 2 ### 默认值 如果数组比左边的变量列表短,这里也不会出现报错。缺少的值被认为是 undefinedjsx let [firstName, surname] = []; alert(firstName); // undefined alert(surname); // undefined 如果我们想要一个“默认”值给未赋值的变量,我们可以使用 = 来提供: jsx // 默认值 let [name = "Guest", surname = "Anonymous"] = ["Julius"]; alert(name); // Julius(来自数组的值) alert(surname); // Anonymous(默认值被使用了) ### 对象解构 jsx let options = { title: "Menu", width: 100, height: 200 }; let {title, width, height} = options; alert(title); // Menu alert(width); // 100 alert(height); // 200 如果我们想把一个属性赋值给另一个名字的变量,比如把 options.width 属性赋值给名为 w 的变量,那么我们可以使用冒号来设置变量名称: jsx let options = { title: "Menu", width: 100, height: 200 }; // { sourceProperty: targetVariable } let {width: w, height: h, title} = options; // width -> w // height -> h // title -> title alert(title); // Menu alert(w); // 100 alert(h); // 200 对于可能缺失的属性,我们可以使用 "="设置默认值,如下所示: jsx let options = { title: "Menu" }; let {width = 100, height = 200, title} = options; alert(title); // Menu alert(width); // 100 alert(height); // 200 如果我们有一个具有很多属性的复杂对象,那么我们可以只提取所需的内容: jsx let options = { title: "Menu", width: 100, height: 200 }; // 仅提取 title 作为变量 let { title } = options; alert(title); // Menu ### 剩余模式(pattern)"…” jsx let options = { title: "Menu", height: 200, width: 100 }; // title = 名为 title 的属性 // rest = 存有剩余属性的对象 let {title, ...rest} = options; // 现在 title="Menu", rest={height: 200, width: 100} alert(rest.height); // 200 alert(rest.width); // 100 ### 智能函数参数 我们可以把所有参数当作一个对象来传递,然后函数马上把这个对象解构成多个变量: jsx // 我们传递一个对象给函数 let options = { title: "My menu", items: ["Item1", "Item2"] }; // ……然后函数马上把对象展开成变量 function showMenu({title = "Untitled", width = 200, height = 100, items = []}) { // title, items – 提取于 options, // width, height – 使用默认值 alert( `${title} ${width} ${height}` ); // My Menu 200 100 alert( items ); // Item1, Item2 } showMenu(options); ## 时间和日期 ## JSON方法和toJson ### JSON.stringify jsx let json = JSON.stringify(value[, replacer, space]) - value要编码的值。 - replacer要编码的属性数组或映射函数 function(key, value)。 - space用于格式化的空格数量 大部分情况,JSON.stringify 仅与第一个参数一起使用。但是,如果我们需要微调替换过程,比如过滤掉循环引用,我们可以使用 JSON.stringify 的第二个参数。 jsx let room = { number: 23 }; let meetup = { title: "Conference", participants: [{name: "John"}, {name: "Alice"}], place: room // meetup 引用了 room }; room.occupiedBy = meetup; // room 引用了 meetup alert( JSON.stringify(meetup, ['title', 'participants', 'place', 'name', 'number']) ); /* { "title":"Conference", "participants":[{"name":"John"},{"name":"Alice"}], "place":{"number":23} } */ 我们可以使用一个函数代替数组作为 replacer jsx let room = { number: 23 }; let meetup = { title: "Conference", participants: [{name: "John"}, {name: "Alice"}], place: room // meetup 引用了 room }; room.occupiedBy = meetup; // room 引用了 meetup alert( JSON.stringify(meetup, function replacer(key, value) { alert(`${key}: ${value}`); return (key == 'occupiedBy') ? undefined : value; })); /* key:value pairs that come to replacer: : [object Object] title: Conference participants: [object Object],[object Object] 0: [object Object] name: John 1: [object Object] name: Alice place: [object Object] number: 23 occupiedBy: [object Object] */ 第一个调用很特别。它是使用特殊的“包装对象”制作的:{"": meetup}。换句话说,第一个 (key, value) 对的键是空的,并且该值是整个目标对象。这就是上面的示例中第一行是 ":[object Object]"的原因 ### JSON.parse jsx let value = JSON.parse(str, [reviver]); str 要解析的 JSON 字符串。 reviver 可选的函数 function(key,value),该函数将为每个 (key, value) 对调用,并可以对值进行转换。 jsx let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}'; let meetup = JSON.parse(str, function(key, value) { if (key == 'date') return new Date(value); return value; }); alert( meetup.date.getDate() ); # 函数进阶 ## 变量作用域,闭包 在 JavaScript 中,每个运行的函数,代码块 {...} 以及整个脚本,都有一个被称为 词法环境(Lexical Environment) 的内部(隐藏)的关联对象。 词法环境对象由两部分组成: 1. 环境记录(Environment Record) —— 一个存储所有局部变量作为其属性(包括一些其他信息,例如 this 的值)的对象。 2. 对 外部词法环境 的引用,与外部代码相关联。 一个“变量”只是 环境记录 这个特殊的内部对象的一个属性。“获取或修改变量”意味着“获取或修改词法环境的一个属性”。 1. 当脚本开始运行,词法环境预先填充了所有声明的变量。 - 最初,它们处于“未初始化(Uninitialized)”状态。这是一种特殊的内部状态,这意味着引擎知道变量,但是在用 let 声明前,不能引用它。几乎就像变量不存在一样。 2. 然后 let phrase 定义出现了。它尚未被赋值,因此它的值为 undefined。从这一刻起,我们就可以使用变量了。 3. phrase 被赋予了一个值。 4. phrase 的值被修改。 函数声明的初始化会被立即完成 这就是为什么我们可以在(函数声明)的定义之前调用函数声明。 正常来说,这种行为仅适用于函数声明,而不适用于我们将函数分配给变量的函数表达式,例如 let say = function(name)... 当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。 所有的函数在“诞生”时都会记住创建它们的词法环境。 ### 闭包 闭包是指内部函数总是可以访问其所在的外部函数中声明的变量和参数,即使在其外部函数被返回(寿命终结)了之后。 ## 函数对象 所以,一个函数本身可以完成一项有用的工作,还可以在自身的属性中附带许多其他功能。 它们创建一个“主”函数,然后给它附加很多其它“辅助”函数。例如,jQuery 库创建了一个名为 $ 的函数。lodash 库创建一个 _ 函数,然后为其添加了 _.add_.keyBy 以及其它属性(想要了解更多内容,参查阅 docs)。实际上,它们这么做是为了减少对全局空间的污染,这样一个库就只会有一个全局变量。这样就降低了命名冲突的可能性。 此外,函数可以带有额外的属性。很多知名的 JavaScript 库都充分利用了这个功能。 如果函数是通过函数表达式的形式被声明的(不是在主代码流里),并且附带了名字,那么它被称为命名函数表达式(Named Function Expression)。这个名字可以用于在该函数内部进行自调用,例如递归调用等。 - length —— 函数定义时的入参的个数。Rest 参数不参与计数。 - name —— 函数的名字。通常取自函数定义,但如果函数定义时没设定函数名,JavaScript 会尝试通过函数的上下文猜一个函数名(例如把赋值的变量名取为函数名)。 我们介绍了它们的一些属性: 函数就是对象。 jsx let sayHi = function func(who) { if (who) { alert(`Hello, ${who}`); } else { func("Guest"); // 使用 func 再次调用函数自身 } }; sayHi(); // Hello, Guest // 但这不工作: func(); // Error, func is not defined(在函数外不可见) ## "new Function" 语法 jsx let func = new Function ([arg1, arg2, ...argN], functionBody); let sum = new Function('a', 'b', 'return a + b'); alert( sum(1, 2) ); // 3 let sayHi = new Function('alert("Hello")'); sayHi(); // Hello 如果我们使用 new Function创建一个函数,那么该函数的 [[Environment]]并不指向当前的词法环境,而是指向全局环境。因此,此类函数无法访问外部(outer)变量,只能访问全局变量。 new Function允许我们将任意字符串变为函数。例如,我们可以从服务器接收一个新的函数并执行它 ### 闭包 ## 调度 jsx let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...) function sayHi(phrase, who) { alert( phrase + ', ' + who ); } setTimeout(sayHi, 1000, "Hello", "John"); // Hello, John ### setTimeout 任何 setTimeout 都只会在当前代码执行完毕之后才会执行。 ### setInterval jsx let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...) ## ??装饰器模式和转发,call/apply jsx func.call(context, arg1, arg2, ...) func.apply(context, args) call 和 apply之间唯一的语法区别是,call 期望一个参数列表,而 apply期望一个包含这些参数的类数组对象。 ### 防抖装饰器 ### 节流装饰器 ## 函数绑定 ### 丢失this 当将对象方法作为回调进行传递,例如传递给 setTimeout,这儿会存在一个常见的问题:“丢失 this”。 jsx let user = { firstName: "John", sayHi() { alert(`Hello, ${this.firstName}!`); } }; setTimeout(user.sayHi, 1000); // Hello, undefined! setTimeout获取到了函数 user.sayHi,但它和对象分离开了。最后一行可以被重写为: jsx let f = user.sayHi; setTimeout(f, 1000); // 丢失了 user 上下文 ### 解决方案 :bind jsx let boundFunc = func.bind(context); jsx let user = { firstName: "John", sayHi() { alert(`Hello, ${this.firstName}!`); } }; let sayHi = user.sayHi.bind(user); // (*) // 可以在没有对象(译注:与对象分离)的情况下运行它 sayHi(); // Hello, John! setTimeout(sayHi, 1000); // Hello, John! // 即使 user 的值在不到 1 秒内发生了改变 // sayHi 还是会使用预先绑定(pre-bound)的值,该值是对旧的 user 对象的引用 user = { sayHi() { alert("Another user in setTimeout!"); } }; ### 偏函数(Partial functions) bind不仅可以绑定 this,还可以绑定参数(arguments)。 jsx let bound = func.bind(context, [arg1], [arg2], ...); 它允许将上下文绑定为 this,以及绑定函数的起始参数。 jsx function mul(a, b) { return a * b; } let double = mul.bind(null, 2); alert( double(3) ); // = mul(2, 3) = 6 alert( double(4) ); // = mul(2, 4) = 8 alert( double(5) ); // = mul(2, 5) = 10 对 mul.bind(null, 2) 的调用创建了一个新函数 double,它将调用传递到 mul,将 null 绑定为上下文,并将 2绑定为第一个参数。并且,参数(arguments)均被“原样”传递。 当我们不想一遍又一遍地重复相同的参数时,partial 非常有用。就像我们有一个 send(from, to)函数,并且对于我们的任务来说,from应该总是一样的,那么我们就可以搞一个 partial 并使用它。 一个函数不能被重绑定(re-bound)。 ## 深入理解箭头函数 ### 箭头函数没有 “this“ 箭头函数没有 this。如果访问 this,则会从外部获取 jsx let group = { title: "Our Group", students: ["John", "Pete", "Alice"], showList() { this.students.forEach( student => alert(this.title + ': ' + student) ); } }; group.showList(); 箭头函数 => 和使用 .bind(this) 调用的常规函数之间有细微的差别: - .bind(this) 创建了一个该函数的“绑定版本”。 - 箭头函数 => 没有创建任何绑定。箭头函数只是没有 thisthis 的查找与常规变量的搜索方式完全相同:在外部词法环境中查找。 ### 箭头函数没有 “arguments” 箭头函数也没有 arguments变量。 # 对象属性配置 ## 属性标志和属性描述符 - configurable — 如果为 true,则此属性可以被删除,这些特性也可以被修改,否则不可以。 - enumerable — 如果为 true,则会被在循环中列出,否则不会被列出。 - writable — 如果为 true,则值可以被修改,否则它是只可读的。 对象属性(properties),除 value 外,还有三个特殊的特性(attributes),也就是所谓的“标志”: 获取属性标志 jsx let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName); let user = { name: "John" }; let descriptor = Object.getOwnPropertyDescriptor(user, 'name'); alert( JSON.stringify(descriptor, null, 2 ) ); /* 属性描述符: { "value": "John", "writable": true, "enumerable": true, "configurable": true } */ 修改属性标志 jsx Object.defineProperty(obj, propertyName, descriptor) 如果该属性存在,defineProperty  会更新其标志。否则,它会使用给定的值和标志创建属性;在这种情况下,如果没有提供标志,则会假定它是 falsejsx let user = {}; Object.defineProperty(user, "name", { value: "John" }); let descriptor = Object.getOwnPropertyDescriptor(user, 'name'); alert( JSON.stringify(descriptor, null, 2 ) ); /* { "value": "John", "writable": false, "enumerable": false, "configurable": false } */ ### 只读 jsx let user = { name: "John" }; Object.defineProperty(user, "name", { writable: false }); user.name = "Pete"; // Error: Cannot assign to read only property 'name' ### 不可枚举 jsx let user = { name: "John", toString() { return this.name; } }; Object.defineProperty(user, "toString", { enumerable: false }); // 现在我们的 toString 消失了: for (let key in user) alert(key); // name ### 不可配置 不可配置的属性不能被删除,它的特性(attribute)不能被修改。 使属性变成不可配置是一条单行道。我们无法通过 defineProperty再把它改回来。 configurable: false 防止更改和删除属性标志,但是允许更改对象的值。 对于不可配置的属性,我们可以将 writable: true更改为 false,从而防止其值被修改(以添加另一层保护)。但无法反向行之。 ## 属性的 getter 和 setter jsx let user = { name: "John", surname: "Smith", get fullName() { return `${this.name} ${this.surname}`; } }; alert(user.fullName); // John Smith jsx let user = { name: "John", surname: "Smith", get fullName() { return `${this.name} ${this.surname}`; }, set fullName(value) { [this.name, this.surname] = value.split(" "); } }; // set fullName 将以给定值执行 user.fullName = "Alice Cooper"; alert(user.name); // Alice alert(user.surname); // Cooper 从外表看,访问器属性看起来就像一个普通属性。这就是访问器属性的设计思想。我们不以函数的方式 调 user.fullName,我们正常 读取它:getter 在幕后运行。 ### 访问器描述符 访问器属性的描述符与数据属性的不同。 对于访问器属性,没有 value 和 writable,但是有 get 和 set 函数。 所以访问器描述符可能有: - get —— 一个没有参数的函数,在读取属性时工作, - set —— 带有一个参数的函数,当属性被设置时调用, - enumerable —— 与数据属性的相同, - configurable —— 与数据属性的相同。 # 原型,继承 ### [[Prototype]] 在 JavaScript 中,对象有一个特殊的隐藏属性 [[Prototype]](如规范中所命名的),它要么为 null,要么就是对另一个对象的引用。该对象被称为“原型” 属性 [[Prototype]] 是内部的而且是隐藏的,但是这儿有很多设置它的方式。 其中之一就是使用特殊的名字 __proto__ jsx let animal = { eats: true }; let rabbit = { jumps: true }; rabbit.__proto__ = animal; // (*) // 现在这两个属性我们都能在 rabbit 中找到: alert( rabbit.eats ); // true (**) alert( rabbit.jumps ); // true 1. 引用不能形成闭环。如果我们试图在一个闭环中分配 __proto__,JavaScript 会抛出错误。 2. __proto__ 的值可以是对象,也可以是 null。而其他的类型都会被忽略。 3. 只能有一个 [[Prototype]] 。一个对象不能从其他两个对象获得继承。 ### “this” 的值 this 根本不受原型的影响。 无论在哪里找到方法:在一个对象还是在原型中。在一个方法调用中,this 始终是点符号 . 前面的对象。 ### for...in jsx let animal = { eats: true }; let rabbit = { jumps: true, __proto__: animal }; // Object.keys 只返回自己的 key alert(Object.keys(rabbit)); // jumps // for..in 会遍历自己以及继承的键 for(let prop in rabbit) alert(prop); // jumps,然后是 eats obj.hasOwnProperty(key):如果 obj  具有自己的(非继承的)名为 key  的属性,则返回 true 几乎所有其他键/值获取方法,例如 Object.keys 和 Object.values 等,都会忽略继承的属性。 它们只会对对象自身进行操作。不考虑 继承自原型的属性。 ## F.prototype - "prototype" 属性仅在设置了一个构造函数(constructor function),并通过 new 调用时,才具有这种特殊的影响。 默认情况下,所有函数都有 F.prototype = {constructor:F} ,所以我们可以通过访问它的 "constructor" 属性来获取一个对象的构造器。 - F.prototype 的值要么是一个对象,要么就是 null:其他值都不起作用。 - F.prototype 属性(不要把它与 [[Prototype]] 弄混了)在 new F 被调用时为新对象的 [[Prototype]] 赋值。 ## 原型方法,没有 proto 的对象 应该使用这些方法来代替 __proto__。 - Object.setPrototypeOf(obj, proto) —— 将对象 obj 的 [[Prototype]] 设置为 proto。 - Object.getPrototypeOf(obj) —— 返回对象 obj 的 [[Prototype]]。 - Object.create(proto, [descriptors]) —— 利用给定的 proto 作为 [[Prototype]] 和可选的属性描述来创建一个空对象。 现代的方法有: __proto__ 被认为是过时且不推荐使用的(deprecated),这里的不推荐使用是指 JavaScript 规范中规定,proto 必须仅在浏览器环境下才能得到支持。 jsx let animal = { eats: true }; // 创建一个以 animal 为原型的新对象 let rabbit = Object.create(animal); alert(rabbit.eats); // true alert(Object.getPrototypeOf(rabbit) === animal); // true Object.setPrototypeOf(rabbit, {}); // 将 rabbit 的原型修改为 {} # 错误处理 # Promise,async/await ## 错误处理 jsx new Promise(function(resolve, reject) { setTimeout(() => { throw new Error("Whoops!"); }, 1000); }).catch(alert); 所有同步错误都会得到处理。 但是这里的错误并不是在 executor 运行时生成的,而是在稍后生成的。因此,promise 无法处理它。 ## Promise API 1. Promise.all(promises) —— 等待所有 promise 都 resolve 时,返回存放它们结果的数组。如果给定的任意一个 promise 为 reject,那么它就会变成 Promise.all 的 error,所有其他 promise 的结果都会被忽略。 2. Promise.allSettled(promises)(ES2020 新增方法)—— 等待所有 promise 都 settle 时,并以包含以下内容的对象数组的形式返回它们的结果: - status"fulfilled" 或 "rejected" - value(如果 fulfilled)或 reason(如果 rejected)。 3. Promise.race(promises) —— 等待第一个 settle 的 promise,并将其 result/error 作为结果返回。 4. Promise.any(promises)(ES2021 新增方法)—— 等待第一个 fulfilled 的 promise,并将其结果作为结果返回。如果所有 promise 都 rejected,Promise.any 则会抛出 [AggregateError](https://developer.mozilla.org/zh/docs/Web/JavaScript/Reference/Global_Objects/AggregateError) 错误类型的 error 实例。 5. Promise.resolve(value) —— 使用给定 value 创建一个 resolved 的 promise。 6. Promise.reject(error) —— 使用给定 error 创建一个 rejected 的 promise。