重学JS第一天


HTML 中的 JavaScript

1.script 元素

  • crossorigin:可选。配置相关请求的 CORS(跨源资源共享)设置。默认不使用 CORS。crossorigin= “anonymous”配置文件请求不必设置凭据标志。crossorigin=”use-credentials”设置凭据 标志,意味着出站请求会包含凭据
  • defer:可选 表示脚本可以延迟到文档完全被解析和显示之后再执行 只对外部脚本文件有效
  • integrity:可选。允许比对接收到的资源和指定的加密签名以验证子资源完整性 果接收到的资源的签名与这个属性指定的签名不匹配,则页面会报错, 脚本不会执行。这个属性可以用于确保内容分发网络(CDN,Content Delivery Network)不会提 供恶意内容

使用了 src 属性的<script>元素不应该再在<script>标签之间再包含其他 js 代码 如果两者都提供的话 浏览器只会下载并执行标本文件 从而忽略行内代码

浏览器会根据特定的设置缓存所有外部连接的 JavaScript 文件 这意味着如果两个页面都用到同个文件 则该文件只需要下载一次 这最终意味着页面加载更快

语言基础

const优先 let次之

使用const声明可以让浏览器运行时强制保持变量不变 也可以让静态代码分析工具提前法相不合法的赋值操作。

数据类型

typeof 操作符

typeof null == object null 被认为是一个对空对象的引用

tips:我们建议在声明变量的同时进行初始化 这样当 typeof返回undefined时我们知道是因为给定的变量尚未声明而不是声明了但没有初始化

tips:当我们定义一个未来将会赋值对象的变量时 应该初始化为 null(可以保持 null 是空对象指针的语义 并与 undefined 区分开

.)

isNaN() 可以用来测试对象 此时会先调用对象的valueOf()方法 然后再确定返回的值是否可以转换为数值 如果不能 再调用toString()方法 再测试其返回值 这通常是 ES 内置函数和操作符的工作方式

数值转换

1
2
Number()
true - 1 || null - 0 || undefined - NaN || Object 先用valueof 如果是NaN 则用toString()

字符串是不可变的 要修改某个变量中的字符串的值 必须先销毁原始的字符串 然后将包括新值的另一个字符串保存到该变量中

用加号操作符给一个值加上一个“”也可以将其转换为字符串

Symbol 类型

符号是原始值 且符号实例唯一 不可变 用于确保对象属性使用唯一标识符 不会发生属性冲突的危险 可以用来创建唯一记号 进而用作非字符串形式的对象属性

符号没有字面量语法 即只要创建Symbol()实例并将其用作对象的新属性 就可以保证它不会覆盖以有的对象属性 无论是符号属性还是字符串属性

Symbol()函数不能与 new 关键字一起作为构造函数使用 这样做是为了避免创建符号包装对象 如果想使用符号包装对象 可以使用Object()函数

1
2
3
let mySymbol = Symbol();
let myWarpSymbol = Object(mySymbol);
console.log(typeof myWarpSymbol); //object

使用全局符号注册表

如果运行时的不同部分需要共享和重用符号实例 可以使用一个字符串作为键 再全局符号注册表中创建并重用符号 需要使用Symbol.for()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let fooGlobalSymbol = Symbol.for('foo');
console.log(fooGolbalSymbol);//symbol
Symbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运
行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同
字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例
let fooGlobalSymbol = Symbol.for('foo'); // 创建新符号
let otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符号
console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true
即使采用相同的符号描述 在全局注册表中定义的符号跟使用Symbol()定义的符号也并不相同
let localSymbol = Symbol('foo');
let globalSymbol = Symbol.for('foo');
console.log(localSymbol === globalSymbol); // false
还可以使用 Symbol.keyFor()来查询全局注册表,这个方法接收符号,返回该全局符号对应的字
符串键。如果查询的不是全局符号,则返回 undefined
// 创建全局符号
let s = Symbol.for('foo');
console.log(Symbol.keyFor(s)); // foo
// 创建普通符号
let s2 = Symbol('bar');
console.log(Symbol.keyFor(s2)); // undefined
如果传给 Symbol.keyFor()的不是符号,则该方法抛出 TypeError
Symbol.keyFor(123); // TypeError: 123 is not a symbol

Object 类型

  • hasOwnProperty(propertyName):判断当前对象实例(不是原型)上是否存在特定的属性
  • isPrototypeOf(object):判断当前对象是否为另一个对象的原型
  • peopertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用 for-in 循环

由于相等和不相等操作符存在类型转换问题 因此推荐使用全等和不全等操作符 这样有助于在代码中保持数据类型的完整性

语句

for-in

为了确保局部变量不被修改 推荐使用 const for-in 不能保证返回对象属性的顺序 如果迭代的变量是 null 或者 undefined 则不执行循环体

1
2
3
for (const propName in window) {
document.write(propName);
}

for-of

for-of 循环会按照可迭代对象的 next()方法产生值的顺序迭代元素。

for-await-of 循环 支持生成 promise 的异步可迭代对象

switch

switch 语句在比较每个条件的值时会使用全等操作符 因此不会强制转换数据类型

变量、作用域与内存

原始值:按值访问

引用值:对该对象的引用而不是实际的对象本身

复制值

  • 1.原始值

在通过变量把原始值赋值给另一个变量时 原始值会被复制到新变量的位置 这两个位置是完全独立的 互不干扰

  • 2.引用值

在把引用值从一个变量赋给另一个变量时 储存在变量中的值也会被复制到新变量所在的位置 区别在于这里复制的值实际上时一个指针 它指向储存在堆内存中的对象那个 操作完成后 两个变量实际上指向同一个对象 因此一个对象上面的变化也会从另一个对象上反映出来

1
2
3
4
let obj1 = new Object();
let obj2 = obj1;
obj1.name = "Nicholas";
console.log(obj2.name); // "Nicholas"

传递参数

ECMAScript 中所有函数的参数都是值传递(ECMAScipt 不可能引用传递) 这就意味着函数外的值会被复制到函数内部的参数中 就像一个变量复制到另一个变量一样 (如果是原始值 就和原始值变量的复制一样 如果是引用值 就和引用值的复制一样)

按值传递参数时 值会被复制到一个局部变量(一个命名参数 即 arguments 对象中的一个槽位)

1
2
3
4
5
6
7
8
function addTen(num) {
//这里的num其实是一个局部变量
num += 10; //不会影响到外部的count
return num;
}
let count = 20;
let result = addTen(count); //30
console.log(count); //20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function setName(obj) { //obj指向的对象保存在全局作用域的堆内存上 所以也会使外部的对象放映这个变化
obj.name = "Nicholas";
}
let person = new Object();
setName(person);
console.log(person.name); // "Nicholas"
----------------------------------//证明为啥不是引用传递
function setName(obj) {
obj.name = "Nicholas"; //影响到了外部的变量
obj = new Object(); //被重写 变成了一个指向本地的指针 函数执行结束后就被销毁了 本来也不会影响到外部的对象
obj.name = "Greg";
}
let person = new Object();
setName(person);
console.log(person.name); // "Nicholas"

函数中的参数就是局部变量

类型判断

使用 typeof 判断原始值 使用 instance of 判断引用类型(由原型链决定)

如果想让整个对象都不能被更改 可以使用 freeze()

垃圾回收

标记清理

当变量进入上下文的时候 将变量加上存在于上下文中的标记 当变量离开上下文时 加上离开上下文的标记

来及回收程序运行的时候 会标记内存中存储的所有变量 然后将所有上下文中的变量以及被在上下文中引用的变量的标记去掉 再次之后再被加上标记的变量就是待删除的 原因是任何在上下文中的变量都访问不到它们了,随后垃圾回收程序做一次内存清理

引用计数

对每个值都记录它被引用的次数 当一个值的引用数为 0 时 就说明没法再访问这个值了 可以安全地回收其内存

内存管理

优化内存占用的最佳手段就是保证再执行代码时只保存必要的数据 如果数据不再必要 就设置为 null 从而释放 这个叫做解除引用(适合全局变量和全局变量的属性 局部变量在超出作用域之后会被自动解除引用)

解除对一个值的引用并不会自动导致相关内存被回收 解除引用的关键在于确保相关的值已经不在上下文中了 因此下一次垃圾回收的时候会被回收

内存泄漏

闭包很容易造成内存泄漏

1
2
3
4
5
6
7
let outer = function () {
let name = "Jake";
return function () {
return name;
};
};
//调用outer()会导致分配给name的内存被泄漏 因为以上代码执行后创建了一个内部闭包 只要返回的参数存在就不能清理name 因为闭包一直引用着它

基本引用类型

引用类型和原始值包装类型(String、Number、Boolean)的主要区别在于对象的生命周期 在通过new 实例化引用类型后 得到的实例会在离开作用域时被销毁 而自动创建的原始值包装对象则指存在于访问它的那行代码执行期间 这意味着不能再运行时给原始值添加属性和方法

1
2
3
let S1 - "some text";
s1.color = 'read';//临时创建一个String对象 第二行运行后就销毁了
console.log(s1.color)//undefined//创建了自己的String对象 但是这个对象没有color属性

在原始值包装类类型的实例上调用 typeof 会返回 object 所有原始值包装对象都会转换为 true

1
2
let obj = new Object("some text");
console.log(obj instanceof String); // true

使用new调用原始值包含在那个类型的构造函数和调用同名的转型函数不一样

1
2
3
4
5
let value = "25";
let number = Number(value); // 转型函数 保存的时一个值为25的原始数智
console.log(typeof number); // "number"
let obj = new Number(value); // 构造函数 保存的是一个Number的实例
console.log(typeof obj); // "object"

toFixed()可以表示有 0-20 个小数的数值

字符串操作方法

  • concat(): 将一个或多 i 个字符串拼接成一个新字符串

    1
    2
    3
    4
    5
    let stringValue = "hello ";
    let result = stringValue.concat("world");
    console.log(result); // "hello world"
    console.log(stringValue); // "hello"
    //concat()方法可以接收任意多个参数,因此可以一次性拼接多个字符串,

    concat()方法一样,slice()、substr() 和 substring()也不会修改调用它们的字符串,而只会返回提取到的原始新字符串值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    let stringValue = "hello world";
    console.log(stringValue.slice(3)); // "lo world"
    console.log(stringValue.substring(3)); // "lo world"
    console.log(stringValue.substr(3)); // "lo world"
    console.log(stringValue.slice(3, 7)); // "lo w"
    console.log(stringValue.substring(3, 7)); // "lo w"
    console.log(stringValue.substr(3, 7)); // "lo worl"
    //slice将负值参数当成字符串长度加上负参数值 substr将第一个负参数当成字符串长度加上该值 substring将所有负参数值都当作0
    let stringValue = "hello world";
    console.log(stringValue.slice(-3)); // "rld"
    console.log(stringValue.substring(-3)); // "hello world"
    console.log(stringValue.substr(-3)); // "rld"
    console.log(stringValue.slice(3, -4)); // "lo w"
    console.log(stringValue.substring(3, -4)); // "hel"
    console.log(stringValue.substr(3, -4)); // "" (empty string)
    1
    2
    3
    4
    5
    6
    let stringValue = "hello world";
    console.log(stringValue.indexOf("o")); // 4
    console.log(stringValue.lastIndexOf("o")); // 7
    let stringValue = "hello world";
    console.log(stringValue.indexOf("o", 6)); // 7
    console.log(stringValue.lastIndexOf("o", 6)); // 4

    判断是否包含另一个字符串的方法:startsWith() endsWith() inclueds()startsWith()检查开始于索引 0 的匹配项,endsWith()检查开始于索 引(string.length - substring.length)的匹配项,而 includes()检查整个字符串

    1
    2
    3
    4
    5
    6
    7
    let message = "foobarbaz";
    console.log(message.startsWith("foo")); // true
    console.log(message.startsWith("bar")); // false
    console.log(message.endsWith("baz")); // true
    console.log(message.endsWith("bar")); // false
    console.log(message.includes("bar")); // true
    console.log(message.includes("qux")); // false

    trim():创建字符串的一个副本 删除亲啊后的所有空格符再返回结果 原字符串不受影响 trimLeft()trimRight()分别从开始和末尾清理空格

    repeat()接收一个参数 表示将字符串复制多少次后返回拼接所有副本后的结果

    padStart()padEnd()方法会复制字符串,如果小于指定长度,则在相应一边填充字符,直至 满足长度条件。

字符串大小写转换:toLowerCase()、toLocaleLowerCase()、toUpperCase()和toLocaleUpperCase()

单例内置对象

URL 编码方式

encodeURI()encodeURIComponent()方法用于编码统一资源标识符(URI),以便传给浏览器

使用 encodeURIComponent()应该比使用 encodeURI()的频率更高, 这是因为编码查询字符串参数比编码基准 URI 的次数更多。

encodeURI()encodeURIComponent()相对的是 decodeURI()decodeURIComponent()

eval()

解释器 接收一个参数 即耀执行的 js 字符串

1
2
eval("console.log('h1')");
//等价于 console.log("h1")

使用 eval 的时候必须慎重 因为这个方法会对 CSS 利用暴露出很大的攻击面 用户可能插入导致你网站或引用奔溃的代码

集合引用类型

在使用对象字面量表示定义对象时 并不会按实际调用 Object 构造函数

from()可用于将类数组结构转换为数组实例 of()用于将一组参数转换为数组实例

Array.from()对数组进行浅复刻

1
2
3
const a1 = [1, 2, 3, 4];
const a2 = Array.form(a1);
alert(a1 === a1) false

Array.from()还可以接收第二个可选的参数 表示直接增强新数组的值 而无需像调用Array.from().map()那样先创建一个中间数组

1
2
3
4
5
6
7
8
9
10
11
const a1 = [1, 2, 3, 4];
const a2 = Array.from(a1, (x) => x ** 2);
const a3 = Array.from(
a1,
function (x) {
return x ** this.exponent;
},
{ exponent: 2 }
);
console.log(a2); // [1, 4, 9, 16]
console.log(a3); // [1, 4, 9, 16]

Array.of()可以把一组参数转换为数组

数组的迭代器方法

keys()返回数组索引的迭代器 values()返回数组元素的迭代器 entries()返回索引/值对的迭代器

使用结构可以非常容易地在循环中拆分键值对

1
2
3
4
5
const a = ["foo", "bar", "baz", "qux"];
for (const [idx, element] of a.entries()) {
alert(idx);
alert(element);
}

fill()静默忽略超出数组边界、零长度以及方向相反的索引范围

copyWithin()会按照指定范围浅复制数组中的部分内容,然后将它们插入到指 定索引开始的位置。

1
2
3
4
5
6
7
8
9
let ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
ints.copyWithin(5); //从 ints 中复制索引 0 开始的内容,插入到索引 5 开始的位置
// 从 ints 中复制索引 5 开始的内容,插入到索引 0 开始的位置
ints.copyWithin(0, 5);
// 从 ints 中复制索引 0 开始到索引 3 结束的内容
// 插入到索引 4 开始的位置
ints.copyWithin(4, 0, 3);
// JavaScript 引擎在插值前会完整复制范围内的值
// 因此复制期间不存在重写的风险

如果数组中某一项是 null 或 undefined,则在 join()、toLocaleString()、 toString()和 valueOf()返回的结果中会以空字符串表示

reverse()和 sort()都会返回调用它们的数组的引用

1
2
3
4
const arr = [1, 3, 2, 5, 4, 6];
console.log([...arr].sort()); //[1,2,3,4,5,6]
console.log(arr); //[1,3,2,5,4,6]
//使用时需要根据是否可以改变原数组判断创建副本保存

concat()方法可以在现有数组全部元素基础上 创建一个新数组。默认打平 可以使用Symbol.isConcatSpreadable阻止打平

1
2
3
4
5
let colors = ["red", "green", "blue"];
let newColors = ["black", "brown"];
newColors[Symbol.isConcatSpreadable] = false;
// 强制不打平数组
let colors2 = colors.concat("yellow", newColors); // ["red", "green", "blue", "yellow", ["black", "brown"]]

slice()用于创建一个包含原有数组中一个或多个元素的新数组。

splice()的主要目的是 在数组中间插入元素

splice()方法始终返回这样一个数组,它包含从数组中被删除的元素(如果没有删除元素,则返 回空数组)

1
2
3
4
5
6
7
8
9
10
let colors = ["red", "green", "blue"];
let removed = colors.splice(0, 1); // 删除第一项
alert(colors); // green,blue
alert(removed); // red,只有一个元素的数组
removed = colors.splice(1, 0, "yellow", "orange"); // 在位置 1 插入两个元素
alert(colors); // green,yellow,orange,blue
alert(removed); // 空数组
removed = colors.splice(1, 1, "red", "purple"); // 插入两个值,删除一个元素
alert(colors); // green,red,purple,orange,blue
alert(removed); // yellow,只有一个元素的数组

搜索和位置方法

ECMAScript 提供两类搜索数组的方法:按严格相等搜索和按断言函数搜索。

1.严格相等

indexOf()、lastIndexOf()和 includes()。

1
2
3
4
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
alert(numbers.indexOf(4)); // 3
alert(numbers.lastIndexOf(4)); // 5
alert(numbers.includes(4)); // true
2.断言函数

find()返回 第一个匹配的元素,findIndex()返回第一个匹配元素的索引 找到第一个匹配后就不再进行

1
2
3
4
5
6
7
8
9
10
11
12
13
const people = [
{
name: "Matt",
age: 27,
},
{
name: "Nicholas",
age: 29,
},
];
alert(people.find((element, index, array) => element.age < 28));
// {name: "Matt", age: 27}
alert(people.findIndex((element, index, array) => element.age < 28)); //0

迭代方法

  • every():对数组每一项都运行传入的函数,如果对每一项函数都返回 true,则这个方法返回 true。
  • filter():对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回。
  • forEach():对数组每一项都运行传入的函数,没有返回值。
  • map():对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组。
  • some():对数组每一项都运行传入的函数,如果有一项函数返回 true,则这个方法返回 true。 这些方法都不改变调用它们的数组

归并方法

reduce()reduceRight()都会迭代数 组的所有项,并在此基础上构建一个最终返回值

1
2
3
let values = [1, 2, 3, 4, 5];
let sum = values.reduce((prev, cur, index, array) => prev + cur);
alert(sum); // 15

Map

Object类型的一个主要差异是Map实例会维护键值对的插入顺序 因此可以根据插入顺序执行迭代操作

映射实例可以提供一个迭代器 能以插入顺序生成[key, value]形式的数组 可以通过 entries()方法取得迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
const m = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"],
]);
console.log([...m]); // [[key1,val1],[key2,val2],[key3,val3]]
//keys()和 values()分别返回以插入顺序生成键和值的迭代器
for (let key of m.keys()) {
alert(key); //key1 key2 key3
}
for (let key of m.values()) {
alert(key); //value1 value2 value3
}

键和值在迭代器遍历时是可以修改的,但映射内部的引用则无法修改。当然,这并不妨碍修改作为 键或值的对象内部的属性,因为这样并不影响它们在映射实例中的身份

1
2
3
4
5
6
7
const m1 = new Map([["key1", "val1"]]);
// 作为键的字符串原始值是不能修改的
for (let key of m1.keys()) {
key = "newKey";
alert(key); // newKey
alert(m1.get("key1")); // val1
}

选择 Object 还是 Map

  • 占用内存:储存当个键值对所占用的内存挥着键的数量线性增加 Map大约可以比Object多储存 50%的键值对
  • 插入性能:涉及到大量插入操作 Map更佳
  • 查找速度:设计大量查找操作 Object更佳
  • 删除性能:设计大量删除操作 选择Map

weak Map

键只能是Object 或者继承自Object的类型 使用非对象设置键会抛出 TypeError 值没有限制类型

1
2
3
4
5
6
7
8
9
10
11
12
13
const key1 = { id: 1 },
key2 = { id: 2 },
key3 = { id: 3 };
// 初始化是全有或全无的操作
// 只要有一个键无效就会抛出错误,导致整个初始化失败
const wm2 = new WeakMap([
[key1, "val1"],
["BADKEY", "val2"],
[key3, "val3"],
]);
// TypeError: Invalid value used as WeakMap key
typeof wm2;
// ReferenceError: wm2 is not defined

WeakMap的键不属于正式的引用 不会组织垃圾回收 但是只要键存在 键值对就会存在于映射中 并被当作对值的引用 因此不会被垃圾回收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const wm = new WeakMap();
wm.set({}, "val");
//et()方法初始化了一个新对象并将它用作一个字符串的键。因为没有指向这个对象的其他引用,
//所以当这行代码执行完成后,这个对象键就会被当作垃圾回收。然后,这个键/值对就从弱映射中消失
//了,使其成为一个空映射。在这个例子中,因为值也没有被引用,所以这对键/值被破坏以后,值本身
//也会成为垃圾回收的目标。
const wm = new WeakMap();
const container = {
key: {},
};
wm.set(container.key, "val");
function removeReference() {
container.key = null;
}
//const wm = new WeakMap();
const container = {
key: {},
};
wm.set(container.key, "val");
function removeReference() {
container.key = null;
}
//这一次,container 对象维护着一个对弱映射键的引用,因此这个对象键不会成为垃圾回收的目
//标。不过,如果调用了 removeReference(),就会摧毁键对象的最后一个引用,垃圾回收程序就可以
//把这个键/值对清理掉。

**WeakMap 实例之所以限制只能用对象作为键,是为了保证只有通过键对象的引用才能取得值**

WeakMap 的用处

1.私有变量
2.DOM 节点数据

因为WeakMap实例不会妨碍垃圾回收 所以非常适合保存关联元数据

1
2
3
4
5
6
7
8
9
10
const m = new Map();
const loginButton document.querySelector('#login');
//给这个节点关联一些元数据
m.set(loginButton, {disabled: true});
//假设登录按钮被从DOM树删除了 但是由于映射中还保存着对按钮的引用 所以对应的DOM节点仍然会逗留在内存中 除非明确将其从映射中删除或者等到映射本身被销毁
//如果使用WeakMap 当节点从DOM树被删除后 垃圾回收程序就会立即释放其内存(假设其他地方没引用的话
const wm = new WeakMap();
const loginButton = document.querySelector('#login');
// 给这个节点关联一些元数据
wm.set(loginButton, {disabled: true});

Set

Set会维护值插入时的顺序 因此支持按顺序迭代

修改集合中的值的属性不会影响到其作为集合值的身份

1
2
3
4
5
6
const s1 = new Set(["vall"]);
for (let value of s1.values()) {
value = "newVal";
console.log(value); //newVal
console.log(s1.has("val")); //true
}

WeakSet可用于给对象打标签

迭代和扩展操作

扩展运算符在对可迭代对象执行浅复刻时特别有用 只需要简单的语法就可以复制整个对象

1
2
3
let arr1 = [1, 2, 3];
let arr2 = [...arr1];
console.log(arr1 === arr2); // false

浅复制意味着只会复制对象的引用

1
2
3
4
let arr1 = [{}];
let arr2 = [...arr1];
arr1[0].foo = "bar";
console.log(arr2[0]); // { foo: 'bar' }

迭代器和生成器

每个迭代器都表示对可迭代对象的一次性有序遍历 不同的迭代器实例直接拿没有联系 只会独立地遍历可迭代对象

迭代器并不与可迭代对象某个时刻的快照绑定 而仅仅是使用游标来记录遍历可迭代的对象的历程 如果可迭代对象在迭代期间被修改了 那么迭代器也会发生相应的变化

1
2
3
4
5
6
7
8
let arr = ["foo", "baz"];
let iter = arr[Symbol.iterator]();
console.log(iter.next()); // { done: false, value: 'foo' }
// 在数组中间插入值
arr.splice(1, 0, "bar");
console.log(iter.next()); // { done: false, value: 'bar' }
console.log(iter.next()); // { done: false, value: 'baz' }
console.log(iter.next()); // { done: true, value: undefined }

迭代器维护着一个指向可迭代对象的引用 因此迭代器会阻止垃圾回收程序回收可迭代对象

自定义一个迭代器(需要将计数器变量放到闭包里 然后通过闭包返回迭代器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Counter {
constructor(limit) {
this.limit = limit;
}
[Symbol.iterator]() {
let count = 1,
limit = this.limit;
return {
next() {
if (count <= limit) {
return { done: false, value: count++ };
} else {
return { done: true, value: undefined };
}
},
};
}
}
let counter = new Counter(3);
for (let i of counter) {
console.log(i);
} //1 2 3

如果迭代器没有关闭 则还可以继续从上次离开的地方继续迭代 比如 数组的迭代器就是不能关闭的

1
2
3
4
5
6
7
8
9
10
11
let a = [1, 2, 3, 4, 5];
let iter = a[Symbol.iterator]();
for (let i of iter) {
console.log(i);
if (i > 2) {
break;
}
} //1 2 3
for (let i of iter) {
console.log(i);
} //4 5

生成器

生成器对象一开始处于暂停执行的状态 具有next()方法 调用这个方法会让生成器开始或恢复执行

1
2
3
4
function* generatorFn() {}
const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
console.log(g.next); // f next() { [native code] }

函数体为 kon 的生成器函数中间不会停留 调用一次next()就会让生成器达到done:true状态

生成器函数只会在初次调用next()方法后开始执行

1
2
3
4
5
6
function* generatorFn() {
console.log("foobar");
}
// 初次调用生成器函数并不会打印日志
let generatorObject = generatorFn();
generatorObject.next(); // foobar

yield 可以让生成器停止和开始执行 遇到关键字后 执行停止 作用域的状态会被保留

1
2
3
4
5
6
7
8
9
function* generatorFn() {
yield "foo";
yield "bar";
return "baz";
}
let generatorObject = generatorFn();
console.log(generatorObject.next()); // { done: false, value: 'foo' }
console.log(generatorObject.next()); // { done: false, value: 'bar' }
console.log(generatorObject.next()); // { done: true, value: 'baz' }

生成器函数内部的执行流程会针对每个生成器对象区分作用域。在一个生成器对象上调用 next() 不会影响其他生成器

对象、类与面向对象编程

Object.assign()接收一个目标对象和一个或多个元对象作为参数 然后将每个原对象中可枚举(Object.propertyIsEnumerable()返回 true)和自有(Object.hasOwnProperty()返回 true)属性复制到目标对象以字符串和符号为键的属性 会被复制。对每个符合条件的属性,这个方法会使用源对象上的[[Get]]取得属性的值,然后使用目标 对象上的[[Set]]设置属性的值。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 简单复制
*/
dest = {};
src = { id: "src" };
result = Object.assign(dest, src);
// Object.assign 修改目标对象
// 也会返回修改后的目标对象
console.log(dest === result); // true
console.log(dest !== src); // true
console.log(result); // { id: src }
console.log(dest); // { id: src }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
dest = {
set a(val) {
console.log(`Invoked dest setter with param ${val}`);
}
};
src = {
get a() {
console.log('Invoked src getter');
return 'foo';
}
};
Object.assign(dest, src);
// 调用 src 的获取方法
// 调用 dest 的设置方法并传入参数"foo"
// 因为这里的设置函数不执行赋值操作
// 所以实际上并没有把值转移过来
console.log(dest); // Invoked src getter"
"Invoked dest setter with param foo" {}
/**
* 对象引用
*/
dest = {};
src = { a: {} };
Object.assign(dest, src);
// 浅复制意味着只会复制对象的引用
console.log(dest); // { a :{} }
console.log(dest.a === src.a); // true
console.log(dest == src)//false

assign()是浅复制 意味着只会复制对象的引用

如果赋值期间出错 操作会中止并退出 同时抛出错误 因此可能只完成部分复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let dest, src, result;
/**
* 错误处理
*/
dest = {};
src = {
a: "foo",
get b() {
// Object.assign()在调用这个获取函数时会抛出错误
throw new Error();
},
c: "bar",
};
try {
Object.assign(dest, src);
} catch (e) {}
// Object.assign()没办法回滚已经完成的修改
// 因此在抛出错误之前,目标对象上已经完成的修改会继续存在:
console.log(dest); // { a: foo }

可计算属性表达式中抛出任何错误都会中断对象创建 如果计算属性的表达式有副作用就要小心 因为如果表达式抛出错误 那么之前完成的计算是不能回滚的

解构并不要求变量必须在解构表达式中说明 不过 如果是事先声明的变量 则赋值表达式必须包含在一对括号中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let personName, personAge;
let person = {
name: "Matt",
age: 27,
};
({ name: personName, age: personAge } = person);
console.log(personName, personAge); // Matt, 27
//vue的写法
const data = reactive({
name: "",
age: 0,
});
const response = {
name: "olddog",
age: 23,
}(({ name: data.name, age: data.age } = response)); //data:{name: 'olddog', age: 23}

嵌套解构

可以通过解构来复制对象属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
let person = {
name: 'Matt',
age: 27,
job: {
title: 'Software engineer'
}
};
let personCopy = {};
({
name: personCopy.name,
age: personCopy.age,
job: personCopy.job
} = person);
// 因为一个对象的引用被赋值给 personCopy,所以修改
// person.job 对象的属性也会影响 personCopy
person.job.title = 'Hacker'
console.log(person);
// { name: 'Matt', age: 27, job: { title: 'Hacker' } }
console.log(personCopy);
// { name: 'Matt', age: 27, job: { title: 'Hacker' } }
--------------------------------
vue的写法
const data = reactive({
name: '',
age: 0,
address:{
home:""
}
})
const response = {
name: 'olddogewqeeqwweq',
age: 23,
address:{
home: 'huilaieqw1341343'
}
}
onMounted(() => {
({name: data.name, age: data.age, address:{home: data.address.home}} = response)//data:{name: 'olddog', age: 23}
response.name = "wdqdw"
response.address.home = '2132'
data.address.home = 'qdewqdeqsw'
console.log(response);
console.log(data)
//改变response不会影响data
})

涉及到多个属性的解构赋值是一个无关输出的顺序化操作 如果一个解构表达式涉及多个赋值 如果开始的赋值成功而后面的赋值出错 则整个赋值表达式只会完成一部分


文章作者: olddog
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 olddog !
  目录