Skip to content

⛩️ JavaScript 笔记

JS 是一门基于原型、头等函数的语言,是一门多范式,动态的,弱类型的,解释型的,基于对象的脚本语言。

简单来说,JS 是一门客户端脚本语言。

第一章:基础语法

包括变量、数据类型、运算符和控制结构。

一、脚本定义 & 注释

1. 脚本定义

内嵌脚本

可以在 html 文档中使用 script 标签嵌入 JS 代码。

html
<script>
  alert('hello javascript');
</script>

外部文件

通过设置 src 属性引入外部 JS 文件。

html
<script src="1.js"></script>

常用属性

  • defer:让浏览器延迟执行脚本,直到文档解析完成(DOM 构建完成)并且在 DOMContentLoaded 事件触发之前。这意味着,如果你的脚本需要在 DOM 完全加载后才能运行,那么你应该使用 defer 属性。需要注意的是,带有 defer 属性的脚本会按照它们在文档中出现的顺序执行。

    html
    <script src="myscript.js" defer></script>
  • async:让浏览器异步加载脚本。这意味着脚本将在下载完成后尽快执行,但是并不会阻塞 HTML 解析,也不会等待其他脚本。如果你的脚本是独立的,并且不依赖于其他脚本或者不被其他脚本依赖,那么你应该使用 async 属性。

    html
    <script src="myscript.js" async></script>

如果 async 和 defer 都没有被指定,那么脚本会在下载后立即执行,这可能会阻塞 HTML 的解析。

注意
如果在一个 <script> 标签中设定了 src / defer / async 属性,那么这个标签内的 JavaScript 代码将不会被执行。这是因为 src 属性和标签体内的脚本是互斥的,一旦设置了 src / defer / async 属性,浏览器就会忽略标签体内的内容。

2. 代码注释

单行注释

html
<script>
  // 这是单行注释
</script>

多行注释

html
<script>
  /*
    这是多行注释体验
    这里也是注释内容
  */
</script>

文档注释

javascript
/**
 * 计算两个数的和。
 *
 * @param {number} a - 第一个数。
 * @param {number} b - 第二个数。
 * @returns {number} 两个数的和。
 */
function add(a, b) {
  return a + b;
}

3. <noscript> 标签

<noscript> 标签定义在用户的浏览器中禁用脚本(如 JavaScript)时显示的替代内容。这可以确保当脚本无法运行或被禁用时,用户仍能获取关于网页的基本信息。

二、变量与常量

1. 命名规则

JS 中的变量是弱类型(变量类型由所引用的值决定),即变量没有类型而值有类型。

变量名以字母、$、_ 开始,后跟字母、数字、_、$。关键字不能用来做变量名。

备注:JS 中变量名也就是标识符可以是中文,但不推荐。

javascript
var $ = 'javascript'; // 合法
var _ = 'javascript'; // 合法

2. 变量

1)声明

使用 var 关键字声明变量。

注意:var 声明的变量只有函数作用域和全局作用域,不存在块级作用域;在定义变量时一定要写关键字,否则变为全局变量。

javascript
var name = 'javascript';

以上代码是声明和赋值的结合。

javascript
var name;
name = 'javascript';

使用 , 可以同时声明多个变量。

javascript
var n = 2,f = 3;
console.log(f);

下面演示了变量可以更换不同类型的数据。

javascript
var bar = 'javascript';
console.log(typeof bar);  // string

var bar = new String('javascript');
console.log(typeof bar);  // object

bar = 18;
console.log(typeof bar);  // number
2)变量提升

在 JavaScript 中,使用 var 关键字声明的变量和函数声明会被提升到它们所在的作用域的顶部。需要注意的是,只有声明本身会被提升,赋值或初始化的操作会保留在原地

javascript
console.log(a); // undefined
var a = 1;
console.log(a);  // 1

由于变量提升,实际执行的代码相当于:

javascript
var a;
console.log(a); // undefined
a = 1;
console.log(a); // 1

下面是 if(false) 中定义的 var 也会发生变量提升,注释掉 if 结果会使用 foo 函数外面定义的 web 变量。

javascript
var web = "https://baidu.com";

function foo() {
  if (false) {
    var web = "雨下田上";
  }
  console.log(web);
}

foo();

由于变量提升,函数 foo 实际上被解释为:

javascript
function foo() {
  var web;
  if (false) {
    web = "雨下田上";
  }
  console.log(web); // undefined
}

var 没有块级作用域多半跟变量提升有关。会导致很多 bug 。

  • if 语句块中的变量泄露。

  • for 循环中的变量泄露。

    javascript
    var arr = [];
    for (var j = 0; j < 3; j++) {
      arr.push(function() {
        console.log(j);
      });
    }
                                                                              
    functions[0](); // 输出: 3
    functions[1](); // 输出: 3
    functions[2](); // 输出: 3

3. 常量

1)声明

使用 const 关键字。声明时必须同时初始化。

2)Object.freeze

const 有个缺点,原始数据类型不能改变,但引用数据类型可以改变,有什么办法可以使引用数据类型不改变。

可以使用 Object.freeze。如果冻结变量后,引用数据类型中的变量也不可以修改了,但不会报错,可以使用严格模式报出错误,提示修改出错。

总结:Object.freeze 是浅层次冻结。

javascript
"use strict"
const INFO = {
  url: 'https://baidu.com',
  port: '8080'
};
Object.freeze(INFO);
INFO.port = '443'; // Cannot assign to read only property
console.log(INFO);
面试常考基础题

var & let & const 区别

重复声明

  • var:允许在同一作用域内重复声明。
  • let 和 const:不允许在同一块级作用域内重复声明。

初始化

  • var 和 let:可以声明时不初始化。
  • const:必须在声明时初始化。

值的修改

  • var 和 let:声明后可以重新赋值。
  • const:声明后不能重新赋值(但对于对象和数组,其内部属性可以修改)。

作用域

  • var:函数作用域或全局作用域。
  • let 和 const:块级作用域。

变量提升

  • var:会被提升到其作用域的顶部,并初始化为 undefined。
  • let 和 const:也会被提升,但不会被初始化(暂时性死区)。

全局对象属性

  • var:在全局作用域声明 / 声明不写 var 时会成为全局对象的属性。
  • let 和 const:不会成为全局对象的属性。

三、数据类型

1. 原始数据类型

1)数值型
[1] 整型 (整数)
  • 十进制表示前缀:无
  • 二进制表示前缀:0b
  • 八进制表示前缀:0(零)
  • 十六进制表示前缀:0x
[2] 浮点 (小数)
  • 直接写小数;3.14
  • 科学计数法,表示较大的数;2.3e3

浮点的精度问题:十进制小数转为二进制小数,大部分无法精确转换;整数不存在这个问题。

[3] JS 中数值的范围

5e324 ~ 1.7976931348623157e+308

如果超过范围,会表示为 Infinity 或者 -Infinity。

[4] 特殊的数值 NaN

NaN (not a number) 是 number 类型。

特点:NaN 与任何数进行任何运算结果都是 NaN、NaN 与任何数都不相等,包括自己。

[5] 相关函数
isFinite()   判断数据是否在范围内, 在范围内返回true, 否则false
isNaN()      判断数据是否是NaN, 是返回true, 否则返回false
其他类型数据转为数值类, 使用Number()函数
    字符串:   纯数字字符串转为对应数字、空字符串转为0, 其他都是NaN
    布尔值:   true->1  false->0
    null:    转为 0
    undefined: 转为 NaN

字符串类型数据转为数值类, 使用parseInt()和parseFloat()函数
    以数字开头或者纯数字,可用于从字符串中提取数字部分
    空字符和其他类型的数据都转为NaN
    parseInt获取小数点之前, 提取整数; parseFloat提取浮点数
2)布尔型

布尔类型包括 true 与 false 两个值,是开发中使用较多的数据类型。

[1] 声明

使用对象形式创建布尔类型。

javascript
console.log(new Boolean(true));   // true
console.log(new Boolean(false));  // false

但建议使用字面量创建布尔类型。

javascript
let bo = true;
[2] 隐式转换

知识点

基本上所有类型都可以隐式转换为 Boolean 类型。

数据类型truefalse
Number / BigInt非 0 的数值±0 、NaN
String非空字符串空字符串
nullnull
undefinedundefined
Object / Symbol所有对象
ps: 引用类型的 Boolean 值永远为真,如对象和数组(空数组也为真)。

用途

① 条件判断简化

javascript
// 更简洁的条件判断
if (userName) {
  // 只有当userName不是空字符串、±0、null、undefined、NaN时才执行
}

// 代替繁琐的显式检查
if (userName !== "" && userName !== null && userName !== undefined) {
  // 相当于上面的简化写法
}

② 空值检测

javascript
// 检查对象属性是否存在且有效
if (user.address) {
  // 属性存在且非空
}

// 检查数组是否包含元素(虽然空数组本身为true)
if (array.length) {
  // 数组不为空
}

③ 使用逻辑运算符

javascript
// 短路求值设置默认值
// 解释:如果第一个操作数为真,直接返回它,不计算第二个操作数
const name = inputName || "默认名称";

// 条件执行函数
// 解释:如果第一个操作数为假,直接返回它,不计算第二个操作数
user.isAdmin && showAdminControls();

// 双重否定获取布尔值
const hasPermission = !!userPermissions;

④ 三元运算符配合

javascript
// 根据值的"真假性"选择显示内容
const message = userCount ? `有${userCount}个用户` : "没有用户";

⑤ 函数参数验证

javascript
function processUser(user) {
  if (!user) {
    throw new Error("需要提供用户对象");
  }
  // 处理用户...
}

注意

当与 boolean 类型比较时,为了进行比较操作,boolean 会转换为数字:true ==> 1 或 false ==> 0。

  • 数值比较:如果使用 Boolean 与数值比较时,会进行隐式类型转换 true 转为 1,false 转为 0。

    javascript
    console.log(3 == true);   // false
    console.log(0 == false);  // true
  • 字符串比较:下面是一个典型的例子,字符串在与 Boolean 比较时,两边都会转换为数值类型后再进行比较。

    javascript
    console.log(Number("雨下田上"));  // NaN
    console.log(Boolean("雨下田上"));  // true
    console.log("雨下田上" == true);  // false,因为 "雨下得上" 转换为 NaN,true 转换为 1,NaN 不等于 1
    console.log("1" == true);  // true,因为 "1" 转换为 1,true 转换为 1,1 等于 1
  • 数组比较:数组的表现与字符串原理一样,会先转换为数值。

    javascript
    console.log(Number([])); // 0
    console.log(Number([3])); // 3
    console.log(Number([1, 2, 3])); // NaN
    console.log([] == false); // true
    console.log([1] == true); // true
    console.log([1, 2, 3] == true); // false,因为 [1, 2, 3] 转换为 NaN,true 转换为 1,NaN 不等于 1
[3] 显式转换

使用 !! 转换布尔类型。

javascript
var bar = '';
console.log(!!bar); // false
bar = 0;
console.log(!!bar); // false
bar = null;
console.log(!!bar); // false
bar = new Date("2020-2-22 10:33");
console.log(!!bar); // true

使用 Boolean 函数可以显式转换为布尔类型。

javascript
var bar = '';
console.log(Boolean(bar)); // false
bar = 0;
console.log(Boolean(bar)); // false
bar = null;
console.log(Boolean(bar)); // false
bar = new Date("2020-2-22 10:33");
console.log(Boolean(bar)); // true
实例操作

下面使用 Boolean 类型判断用户的输入,并给出不同的反馈。这里用到了字符串的隐式转换。

javascript
while (true) {
  var n = prompt("请输入现在的年份").trim();
  if (!n) continue;
  alert(n == 2024 ? "回答正确" : "答案错误!");
  break;
}
3)String

字符串类型是使用非常多的数据类型,也是相对简单的数据类型。

[1] 声明

使用对象形式创建字符串。

javascript
let bar = new String('xiaoWang');
// 获取字符串长度
console.log(bar.length);
// 获取字符串
console.log(bar.toString());

字面量创建。字符串使用单、双引号包裹。

javascript
let content = '雨下田上';
console.log(content);

备注:使用字面量创建的对象也可以使用字符串方法,原因是有包装类的存在。

[2] 转义序列或转义字符

转义字符是一种特殊的字符序列,由反斜杠 \ 开始,后跟一个或多个字符,这些序列表示一些不能直接输入的字符,或者有特殊含义的字符。

符号说明
\t制表符
\n换行
\\斜杠符号
\'单引号
\"双引号
[3] 常用属性与方法

记忆口诀
拼 ==> 转 ==> 空 ==> 截 ==> 替 ==> 分 ==> 找 ==> 获 ==> 遍
拼接 大小写转换 移除空白 截取字符串 替换字符串 分割字符串 查找字符串 获取单字符 遍历字符串

获取长度

使用 length 属性可以获取字符串长度。

javascript
console.log("https://xiaoWang.com".length);

字符串的拼接

① 操作符:这是最常见的字符串拼接方法。

javascript
let str1 = "Hello, ";
let str2 = "World!";
let result = str1 + str2;
console.log(result); // 输出:Hello, World!

② += 操作符:这个操作符可以将一个字符串添加到另一个字符串的末尾。

javascript
let str = "Hello, ";
str += "World!";
console.log(str); // 输出:Hello, World!

③ concat() 方法:这个方法可以用来拼接两个或多个字符串。

javascript
let str1 = "Hello, ";
let str2 = "World!";
let result = str1.concat(str2);
console.log(result);  // 输出:Hello, World!

④ 模板字符串(Template Literals):在 ES6 中,可以使用模板字符串来拼接字符串。模板字符串使用反引号包围,并可以包含由美元符号和花括号(${})包围的表达式。

javascript
let str1 = "Hello, ";
let str2 = "World!";
let result = `${str1}${str2}`;
console.log(result); // 输出:Hello, World!

大小写转换

将字符转换成大写格式。

javascript
console.log('xiaoWang.com'.toUpperCase());

转字符为小写格式。

javascript
console.log('xiaoWang.com'.toLowerCase());

移除空白

使用 trim 删除字符串左右的空白字符。

javascript
let str = '   xiaoWang.com  ';
console.log(str.length);
console.log(str.trim().length);

使用 trimLeft 删除左边空白,使用 trimRight 删除右边空白。

javascript
let name = " javascript ";
console.log(name);
console.log(name.trimLeft());
console.log(name.trimRight());

获取单字符

根据从 0 开始的位置获取字符。

javascript
console.log('javascript'.charAt(3))

使用数字索引获取字符串。

javascript
console.log('javascript'[3])

截取字符串

使用 slice、substring、substr (历史遗留) 函数都可以截取字符串。

注意:从左往右,开始索引为 0;从右往左,开始索引为 -1。

  • slice、substring(左闭右开)

    slice 方法接受两个参数,分别是开始和结束的索引位置。如果参数是负数,那么它表示从字符串的末尾开始计数的位置。它不会交换两个参数的位置(同符号参数,第一个参数必须小于第二个参数才有值;不同符号,第一个参数必须大于第二个参数才有值)。

    substring 会将任何负数或 NaN 参数视为 0。如果 substring() 的第一个参数大于第二个参数,它会交换两个参数的位置。例如,substring(3, 1) 会被视为 substring(1, 3)

  • substr 第二个参数指定获取字符数量。第一个参数为开始索引,且可以为负数。

javascript
let bar = 'https://happy.eu.org';
console.log(bar.slice(3)); // ps://happy.eu.org
console.log(bar.substr(3)); // ps://happy.eu.org
console.log(bar.substring(3)); // ps://happy.eu.org

console.log(bar.slice(3, 6)); // ps:
console.log(bar.slice(3, 0)); // ""
console.log(bar.slice(-1,-6)); // ""
console.log(bar.substring(3, 6)); // ps:
console.log(bar.substring(3, 0)); // htt 较小的做为起始位置
console.log(bar.substr(3, 6)); // ps://h

console.log(bar.slice(3, -1)); // ps://happy.eu.or 第二个为负数表示从后面算的字符
console.log(bar.slice(-1,3)); // ""
console.log(bar.slice(-2)); // rg 从末尾取
console.log(bar.substring(3, -9)); // htt 负数转为0
console.log(bar.substr(-3, 2)); // or 从后面第三个开始取两个

查找字符串

从开始获取字符串位置,检测不到时返回 -1。

javascript
// 第一个参数:需要查找的字符串  第二个参数:查找起始位置开始往后
let str = "Hello, World!";
let pos = str.indexOf("World");
console.log(pos); // 输出:7

搜索该字符串并返回指定子字符串最后一次出现的索引。它可以接受一个可选的结束索引位置,返回指定子字符串在小于或等于指定数字的索引中的最后一次出现的位置。

javascript
// 第一个参数:需要查找的字符串  第二个参数:查找起始位置开始往前
let str = "Hello, World! World!";
let pos = str.lastIndexOf("World");
console.log(pos); // 输出:14
console.log(str.lastIndexOf("World", 13)); // 输出:7 (从索引13开始向前查找)

search 方法用于检索字符串中指定的子字符串,也可以使用正则表达式搜索。

javascript
let str = "Hello, World!";
let pos = str.search("World");
console.log(pos); // 输出:7

includes 字符串中是否包含指定的值,第二个参数指查找开始位置。

javascript
console.log('https://happy.eu.org'.includes('u')); // true
console.log('https://happy.eu.org'.includes('h', 11)); // false

startsWith 是否是指定位置开始,第二个参数为查找的开始位置(闭区间)。

javascript
console.log('xiaoWang.com'.startsWith('x')); // true
console.log('xiaoWang.com'.startsWith('i', 1)); // true

endsWith 是否是指定位置结束,第二个参数为查找的结束位置(开区间)。

javascript
console.log('xiaoWang.com'.endsWith('com')); // true
console.log('xiaoWang.com'.endsWith('xi', 2)); // true

下面是查找关键词的示例。

javascript
const words = ["php", "css"];
const title = "我在学习php与css知识";
const status = words.some(word => {
  return title.includes(word);
});
console.log(status);

替换字符串

replace 方法用于字符串的替换操作。

javascript
let name = "xiaoWang.com";
web = name.replace("xiaoWang", "lisi");
console.log(web); // lisi.com

默认只替换一次,如果全局替换需要使用正则。

javascript
let str = "2023/02/12";
console.log(str.replace(/\//g, "-"));

使用正则表达式完成替换。

javascript
let res = "xiaoWang.com".replace(/w/g, str => {
  return "@";
});
console.log(res);

使用字符串替换来生成关键词链接。

javascript
const word = ["php", "css"];
const content = "我在学习php与css知识";
const title = word.reduce((pre, word) => {
  return pre.replace(word, `<a href="?w=${word}">${word}</a>`);
}, content);
document.body.innerHTML += title;

分割字符串

split(separator, limit);

javascript
const myString = "Hello World. How are you doing?";
const splits = myString.split(" ", 3);

console.log(splits); // ["Hello", "World.", "How"]

遍历字符串

① for 循环:最基本的方法是使用 for 循环,通过索引访问每个字符。

javascript
let str = "Hello, World!";
for(let i = 0; i < str.length; i++) {
  console.log(str[i]);
}

② for...of 循环:这是一种更现代,更简洁的方式来遍历字符串的每个字符。

javascript
let str = "Hello, World!";
for(let char of str) {
  console.log(char);
}

③ split() 和 forEach():可以将字符串转换为字符数组,然后使用数组的 forEach 方法。

javascript
let str = "Hello, World!";
str.split('').forEach(char => {
  console.log(char);
});

④ Array.from() 和 forEach():Array.from() 可以将字符串转换为字符数组,然后使用数组的 forEach 方法。

javascript
let str = "Hello, World!";
Array.from(str).forEach(char => {
  console.log(char);
});
4)Undefined

undefined 表示 “缺少值”,即此处应该有值,但没有定义。

① 定义了变量没有给值,显示 undefined。

② 定义了形参,没有传实参,显示 undefined。

③ 对象属性名不存在时,显示 undefined。

④ 函数没有写返回值,即没有写 return,拿到的是 undefined。

⑤ 写了 return,但没有赋值,拿到的是 undefined。

例子

对声明但未赋值的变量返回类型为 undefined 表示值未定义。

javascript
let bar;
console.log(typeof bar); // undefined

对未声明的变量使用会报错,但判断类型将显示 undefined。

javascript
console.log(typeof yxts); // undefined
console.log(yxts); // 报错 Uncaught ReferenceError: yxts is not defined

5)NULL

null 表示 “没有对象”,即该处不应该有值。

① 作为函数的参数,表示该函数的参数不是对象。

② 作为对象原型链的终点。

③ 获取 DOM 获取不到的时候。

阮一峰:undefined 与 null 的区别

6)Symbol
[1] 是什么?

Symbol 是一种基本数据类型,引入于 ECMAScript 6(ES6)标准。

Symbol 表示独一无二的标识符,每个 Symbol 值都是唯一的,可以用作对象属性的键。

[2] 创建对象

使用 Symbol 创建一个新的 Symbol 值很简单,只需调用全局的 Symbol() 函数即可,例如:

javascript
// 创建带描述的 Symbol
const sym1 = Symbol('用户ID');
const sym2 = Symbol('订单状态');
const sym3 = Symbol(); // 无描述
const sym4 = Symbol.for("1111");
// 获取描述
console.log(sym1.description); // "用户ID"
console.log(sym2.description); // "订单状态" 
console.log(sym3.description); // undefined
console.log(sym4.description); // 1111

Symbol 不可以添加属性。

javascript
let bar = Symbol();
bar.name = "张三";
console.log(bar.name); // 操作会抛出错误(在严格模式下)或者被忽略(在非严格模式下)

还可以使用 Symbol.for() 创建 Symbol 对象。

javascript
let symbol1 = Symbol.for("张三");
let symbol2 = Symbol.for("李四");
console.log(symbol1 == symbol2); // 输出:false

在 JavaScript 中,Symbol.for()Symbol() 都可以用来创建 Symbol 值,但它们之间存在一些重要的区别。

Symbol() 函数会创建一个全新的、唯一的 Symbol 值。每次调用 Symbol() 都会返回一个不同的 Symbol 值,即使为它们提供了相同的描述。

javascript
let symbol1 = Symbol('mySymbol');
let symbol2 = Symbol('mySymbol');

console.log(symbol1 === symbol2); // 输出: false

Symbol.for() 方法则不同,它首先会在全局 Symbol 注册表中搜索是否存在一个键为给定字符串的 Symbol 值。如果存在,则返回该 Symbol 值;如果不存在,则创建一个新的 Symbol 值,并使用给定的字符串作为键将其添加到全局 Symbol 注册表中,然后返回该 Symbol 值。因此,对于相同的字符串,Symbol.for() 总是返回相同的 Symbol 值。

javascript
let symbol1 = Symbol.for('mySymbol');
let symbol2 = Symbol.for('mySymbol');

console.log(symbol1 === symbol2); // 输出: true

在上面的代码中,symbol1 和 symbol2 是相同的 Symbol 值,因为它们都是使用 Symbol.for() 从全局 Symbol 注册表中获取的。

总的来说,Symbol()Symbol.for() 的主要区别在于,Symbol() 每次都会创建一个全新的 Symbol 值,而 Symbol.for() 则会在全局 Symbol 注册表中重用或创建 Symbol 值。

使用 Symbol.for() 还可以用 Symbol.keyFor 获取字符串。Symbol.keyFor 根据使用 Symbol.for 登记的 Symbol 返回描述,如果找不到返回 undefined。

javascript
let symbol1 = Symbol.for("张三");
console.log(Symbol.keyFor(symbol1)); // 张三

let symbol2 = Symbol("张三");
console.log(Symbol.keyFor(symbol2)); // undefined
[3] 应用场景

Symbol 值可以用作对象属性的键,它们不会被隐式转换为字符串,因此可以确保属性的唯一性。

Symbol 声明和访问使用 [](变量)形式操作。不能使用 . 语法,因为 . 语法是操作字符串属性的。

下面写法是错误的,会将 symbol 当成字符串 symbol 处理。

javascript
let symbo = Symbol("张三");
let obj = {
  symbo: "JavaScript"
};
console.log(obj); // {"symbo": "JavaScript"}

正确写法是以 [] 变量形式声明和访问。

javascript
let symbol = Symbol("张三");
let obj = {
  [symbol]: "JavaScript"
};
console.log(obj[symbol]); // JavaScript

2. 引用数据类型

对象(数组、Set、Map)、函数(类、构造函数)。

1)数组
[1] 声明数组

① 使用对象方式创建数组。

javascript
console.log(new Array(1, '张三', '李四')); // [1, '张三', '李四']

② 使用字面量创建是推荐的简单作法。

javascript
const array = ['张三', '李四'];

多维数组定义。

javascript
const array = [["张三"], ["李四"]];
console.log(array[1][0])

③ Array.of 方法:创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型。

javascript
let arr = Array.of(1, 2, 3); // [1, 2, 3]

④ Array.from 方法:从一个类似数组或可迭代的对象中创建一个新的数组实例。

javascript
let arr = Array.from('Hello'); // ['H', 'e', 'l', 'l', 'o']

在实际开发过程中需要注意:

① 数组是引用类型,可以使 const 声明的数组中的值被修改。

javascript
const array = ["张三", "李四"];
array.push("王五");
console.table(array);

② 下面直接设置 3 号数组,会将 1/2 索引的数组定义为 undefined。

javascript
let arr = ["张三"];
arr[3] = "李四";
console.log(arr.length); // 4

③ 声明多个空元素的数组。

javascript
let arr = new Array(3);
console.log(arr.length);
console.log(arr);

我就想创建 1 个元素的数组,除了字面量声明,还有 Array.of(),使用 Array.ofnew Array 不同是设置一个参数时不会创建空元素数组。

javascript
let arr = Array.of(3);
console.log(arr); // [3]
[2] 属性

使用 length 属性可以获取数组元素数量。

javascript
let arr = ["张三", "李四"];
console.log(arr.length); // 2
[3] 方法

类型检测

javascript
console.log(Array.isArray([1, "张三", "李四"])); // true
console.log(Array.isArray(9)); // false

类型转换

数组转化为字符串

  1. 大部分数据类型都可以使用 .toString() 函数转换为字符串。

    javascript
    console.log(([1, 2, 3]).toString()); // 1,2,3
  2. 使用 join 连接为字符串。

    javascript
    console.log([1, 2, 3].join("-")); // 1-2-3
  3. 使用函数 String 转换为字符串。

    javascript
    console.log(String([1, 2, 3]));

字符串转换为数组

  1. string 类中的 split() 方法。

  2. 使用解构语法。

    javascript
    [...arr] = "kmfgj"
  3. Array.from()

    第一个参数为要转换的数据,第二个参数为类似于 map 函数的回调方法。

    javascript
    let str = '张三';
    console.log(Array.from(str)); // ["张", "三"]

    为对象设置 length 属性后也可以转换为数组,但要下标为数值或数值字符串。

    javascript
    let user = {
      0: '张三',
      '1': 18,
      length: 2
    };
    
    console.log(Array.from(user)); // ["张三", 18]

    DOM 元素转换为数组后来使用数组函数,第二个参数类似于 map 函数的方法,可对数组元素执行函数处理。

    html
    <body>
      <button message="张三">button</button>
      <button message="yxts">button</button>
    </body>
    
    <script>
      let btns = document.querySelectorAll('button');
      console.log(btns); // 包含 length 属性
      Array.from(btns, (item) => {
        item.style.background = 'red';
      });
    </script>

增加 / 弹出元素

push:压入元素,直接改变原数组,返回值为数组元素数量。

javascript
let arr = ["小李", "老王"];
let bar = ["李四"];
bar.push(...arr); // 等价于 bar.push("小李", "老王");
console.log(bar); // ["李四", "小李", "老王"]

pop:移除数组的最后一个元素,并返回该元素。

javascript
let arr = ["小李", "老王"];
console.log(arr.pop()); // 老王
console.log(arr); // ["小李"]

// 通过修改 length 删除最后一个元素
let arr = [0, 1, 2, 3, 4, 5, 6];
arr.length = arr.length - 1;
console.log(arr); // [0, 1, 2, 3, 4, 5]

unshift:向数组的开头添加一个或更多元素,并返回新的长度。

javascript
let arr = ["小李", "老王"];
console.log(arr.unshift('张三', '李四')); // 4
console.log(arr); // ['张三', '李四', "小李", "老王"]

shift:把数组的第一个元素从其中删除,并返回第一个元素的值。

javascript
let arr = ["小李", "老王"];
console.log(arr.shift()); // 小李
console.log(arr); // ["老王"]

slice:从数组中截取部分元素组合成新数组(并不会改变原数组),不传第二个参数时截取到数组的最后元素。

javascript
let arr = [0, 1, 2, 3, 4, 5, 6];
console.log(arr.slice(1, 3)); // [1,2]

不设置参数是为获取所有元素。相当于复制一个新数组。

javascript
let arr = [0, 1, 2, 3, 4, 5, 6];
console.log(arr.slice()); // [0, 1, 2, 3, 4, 5, 6]

splice:可以添加、删除、替换数组中的元素,会对原数组进行改变,返回值为删除的元素。

删除数组元素第一个参数为从哪开始删除,第二个参数为删除的数量。

javascript
let arr = [0, 1, 2, 3, 4, 5, 6];
console.log(arr.splice(1, 3)); // 返回删除的元素 [1, 2, 3]
console.log(arr); // 删除数据后的原数组 [0, 4, 5, 6]

通过指定第三个参数来设置在删除位置添加的元素。

javascript
let arr = [0, 1, 2, 3, 4, 5, 6];
console.log(arr.splice(1, 3, '张三', '李四')); // [1, 2, 3]
console.log(arr); // [0, "张三", "李四", 4, 5, 6]

向末尾添加元素。

javascript
let arr = [0, 1, 2, 3, 4, 5, 6];
console.log(arr.splice(arr.length, 0, '张三', '李四')); // []
console.log(arr); // [0, 1, 2, 3, 4, 5, 6, "张三", "李四"]

向数组前添加元素。

javascript
let arr = [0, 1, 2, 3, 4, 5, 6];
console.log(arr.splice(0, 0, '张三', '李四')); // []
console.log(arr); // ["张三", "李四", 0, 1, 2, 3, 4, 5, 6]

数组元素位置调整函数。

javascript
function move(array, before, to) {
  if (before < 0 || to >= array.length) {
    console.error("指定位置错误");
    return;
  }
  const newArray = [...array];
  const elem = newArray.splice(before, 1);
  newArray.splice(to, 0, ...elem);
  return newArray;
}
const array = [1, 2, 3, 4];
console.table(move(array, 0, 3));

清空数组

将数组值修改为 [] 可以清空数组。如果有多个引用时,数组在内存中存在被其他变量引用。

javascript
let user = ["张三", "李四"];
let cms = user;
user = [];
console.log(user); // []
console.log(cms); // ["张三", "李四"]

将数组 length 设置为 0 也可以清空数组。

javascript
let user = ["张三", "李四"];
user.length = 0;
console.log(user);

使用 splice 方法删除所有数组元素。

javascript
let user = ["张三", "李四"];
user.splice(0, user.length);
console.log(user);

使用 pop / shift 删除所有元素,来清空数组。

javascript
let user = ["张三", "李四"];
while (user.pop()) {}
console.log(user);

合并数组

join:使用 join 连接成字符串。

javascript
let arr = ["张三", "李四"];
console.log(arr.join('-')); // 张三-李四

concat:用于连接两个或多个数组。元素是值类型的是复制操作,如果是引用类型还是指向同一对象(浅拷贝操作)。

javascript
let arr = ["张三", "李四"];
let as = [1, 2];
let cms = [3, 4];
console.log(array.concat(as, cms)); // ["张三", "李四", 1, 2, 3, 4]

也可以使用扩展语法实现连接。

javascript
console.log([...array, ...arr, ...cms]);

copyWithin:从数组中复制一部分到同数组中的另外位置。

语法说明

javascript
array.copyWithin(target, start, end)

参数说明

参数描述
target必需。复制到指定目标索引位置。
start可选。元素复制的起始位置。开区间。
end可选。停止复制的索引位置 (默认为 array.length) 。如果为负值,表示倒数。闭区间。
javascript
// 例子一
let arr = [1, 2, 3, 4, 5];
arr.copyWithin(0, 3); // 将索引为 3 及之后的元素复制到索引为 0 的位置上
console.log(arr); // 输出: [4, 5, 3, 4, 5]

// 例子二
const arr = [1, 2, 3, 4];
console.log(arr.copyWithin(2, 0, 2)); // [1, 2, 1, 2]

排序

arr.sort([compareFunction]);

js
let arr = [
  {name: "John", age: 23},
  {name: "Doe", age: 46},
  {name: "Smith", age: 10},
  {name: "Bruce", age: 13}
];

arr.sort(function(a, b) {
  return a.age - b.age;
});

console.log(arr); 
// 输出: [{name: "Smith", age: 10}, {name: "Bruce", age: 13}, {name: "John", age: 23}, {name: "Doe", age: 46}]

sort 底层逻辑:

  • 返回负数:a 排在 b 前面
  • 返回正数:a 排在 b 后面
  • 返回零:保持原有顺序

现在看看 a - b 和 b - a:

  • a - b(升序排列):
    • 当 a < b 时:结果为负,a 排在 b 前
    • 当 a > b 时:结果为正,a 排在 b 后
    • 最终效果:从小到大排序
  • b - a(降序排列):
    • 当 a < b 时:结果为正,a 排在 b 后
    • 当 a > b 时:结果为负,a 排在 b 前
    • 最终效果:从大到小排序

reverse() 方法将数组中元素的位置颠倒,并返回该数组。

查找元素

indexOf:从前向后查找元素出现的位置,如果找不到返回 -1。

javascript
let arr = [7, 3, 2, 8, 2, 6];
console.log(arr.indexOf(2)); // 2 从前面查找2出现的位置

如下面代码一下,使用 indexOf 查找字符串将找不到,因为 indexOf 类似于 === 是严格类型约束。

javascript
let arr = [7, 3, 2, '8', 2, 6];
console.log(arr.indexOf(8)); // -1

第二个参数用于指定查找开始位置。可以为负数,表示倒着查找。

javascript
let arr = [7, 3, 2, 8, 2, 6];
// 从第四个元素开始向后查找
console.log(arr.indexOf(2, 3)); // 4

includes:查找字符串返回值是布尔类型更方便判断。

javascript
let arr = [7, 3, 2, 6];
console.log(arr.includes(6)); // true

我们来实现一个 includes 函数,来加深对 includes 方法的了解。

javascript
function includes(array, item) {
  for (const value of array)
    if (item === value) return true;
  return false;
}

console.log(includes([1, 2, 3, 4], 3)); // true

find:找到后会把值返回出来。如果找不到返回值为 undefined。返回第一次找到的值,不继续查找。

javascript
let arr = ["张三", "李四", "王五"];

let find = arr.find(function(item) {
  return item == "李四";
});

console.log(find); // 李四

使用 includes 等不能查找引用类型,因为它们的内存地址是不相等的。

javascript
const user = [{ name: "李四" }, { name: "张三" }, { name: "王五" }];
const find = user.includes({ name: "王五" });
console.log(find);

find 可以方便的查找引用类型。

javascript
const user = [{ name: "李四" }, { name: "张三" }, { name: "王五" }];
const find = user.find(user => (user.name = "王五"));
console.log(find);

findIndex:findIndex 与 find 的区别是返回索引值,参数也是 : 当前值,索引,操作数组。查找不到时返回 -1。

javascript
let arr = [7, 3, 2, '8', 2, 6];

console.log(arr.findIndex(function (v) {
  return v == 8;
})); // 3

find 原理

下面使用自定义函数。

javascript
let arr = [1, 2, 3, 4, 5];
function find(array, callback) {
  for (const value of array) {
    if (callback(value) === true) return value;
  }
  return undefined;
}

let res = find(arr, function(item) {
  return item == 23;
});
console.log(res);

下面添加原型方法实现。

javascript
Array.prototype.findValue = function(callback) {
  for (const value of this) {
    if (callback(value) === true) return value;
  }
  return undefined;
};

let arr = [1, 2, 3, 4, 5];
let re = arr.findValue(function(item) {
  return item == 2;
});
console.log(re);

高级方法

map() 方法用于对数组中的每个元素执行一个函数,并返回一个新的数组,新数组中的元素是原始数组元素经过函数处理后的结果。

map() 方法不会改变原始数组,而是返回一个新的数组。

map() 方法中的回调函数接受三个参数:当前元素、当前元素的索引和数组本身。

javascript
let numbers = [1, 2, 3, 4, 5];

let squares = numbers.map(function(num) {
  return num * num;
});

console.log(squares);  // 输出 [1, 4, 9, 16, 25]

filter() 方法用于创建一个新的数组,新数组中的元素是通过在原始数组中应用一个测试函数并返回 true 的所有元素。

filter() 方法不会改变原始数组,而是返回一个新的数组。

filter() 方法中的回调函数接受三个参数:当前元素、当前元素的索引和数组本身。

javascript
let numbers = [1, 2, 3, 4, 5];

let evenNumbers = numbers.filter(function(num) {
  return num % 2 === 0;
});

console.log(evenNumbers);  // 输出 [2, 4]

reduce() 方法是对数组中的每个元素执行一个 reducer 函数(升序执行,也就是按照索引顺序),将其结果汇总为单个输出值。

reduce() 方法接受两个参数:一个 reducer 函数和一个可选的初始值。

reducer 函数接受四个参数:累计器(accumulator)、当前值(currentValue)、当前索引(currentIndex)和源数组(array)。在每次调用 reducer 函数时,累计器的值是上一次调用 reducer 函数的返回值(或者是提供的初始值)。

需要注意的是,如果没有提供 reduce() 方法的第二个参数(初始值),则会使用数组的第一个元素作为初始值,并从数组的第二个元素开始执行 reducer 函数。

javascript
let numbers = [1, 2, 3, 4, 5];

let sum = numbers.reduce(function(accumulator, currentValue) {
  return accumulator + currentValue;
}, 0);

console.log(sum);  // 输出 15

find() 方法返回数组中满足提供的测试函数的第一个元素的值。如果没有元素满足测试函数,则返回 undefined。

find() 方法中的回调函数接受三个参数:当前元素、当前元素的索引和数组本身。

javascript
let numbers = [1, 2, 3, 4, 5];

let firstEvenNumber = numbers.find(function(num) {
  return num % 2 === 0;
});

console.log(firstEvenNumber);  // 输出 2

findIndex() 方法返回数组中满足提供的测试函数的第一个元素的索引。如果没有元素满足测试函数,则返回 -1。

findIndex() 方法中的回调函数接受三个参数:当前元素、当前元素的索引和数组本身。

javascript
let numbers = [1, 2, 3, 4, 5];

let firstEvenNumberIndex = numbers.findIndex(function(num) {
  return num % 2 === 0;
});

console.log(firstEvenNumberIndex);  // 输出 1

Array.prototype.some() 方法用于测试数组中是否有元素通过由提供的函数实现的测试。它返回一个 Boolean 值。

javascript
const array = [1, 2, 3, 4, 5];

// 检查数组中是否有元素大于 3
const isGreaterThanThree = array.some(num => num > 3);

console.log(isGreaterThanThree); // 输出:true

some() 方法遍历数组中的每个元素,然后传递给箭头函数 num => num > 3。如果有任何元素使得这个函数返回 true,那么 some() 方法就会立即停止遍历并返回 true。否则,如果没有元素使得这个函数返回 true,那么 some() 方法就会返回 false。

循环遍历

for:根据数组长度结合 for 循环来遍历数组。

javascript
let lessons = [
  {title: '媒体查询响应式布局',category: 'css'},
  {title: 'FLEX 弹性盒模型',category: 'css'},
  {title: 'MYSQL多表查询随意操作',category: 'mysql'}
];

for (let i = 0; i < lessons.length; i++) {
  lessons[i] = `胖虎: ${lessons[i].title}`;
}

console.log(lessons);

forEach:使函数作用在每个数组元素上,但是没有返回值。

下面例子是截取标签的五个字符。

javascript
let lessons = [
  {title: '媒体查询响应式布局',category: 'css'},
  {title: 'FLEX 弹性盒模型',category: 'css'},
  {title: 'MYSQL多表查询随意操作',category: 'mysql'}
];

lessons.forEach((item, index, array) => {
  item.title = item.title.substr(0, 5);
});

console.log(lessons);

for / in:遍历时的 key 值为数组的索引。

javascript
let lessons = [
  {title: '媒体查询响应式布局',category: 'css'},
  {title: 'FLEX 弹性盒模型',category: 'css'},
  {title: 'MYSQL多表查询随意操作',category: 'mysql'}
];

for (const key in lessons) {
  console.log(`标题: ${lessons[key].title}`);
}

for / of:与 for/in 不同的是 for/of 每次循环取其中的值而不是索引。

javascript
let lessons = [
  {title: '媒体查询响应式布局',category: 'css'},
  {title: 'FLEX 弹性盒模型',category: 'css'},
  {title: 'MYSQL多表查询随意操作',category: 'mysql'}
];

for (const item of lessons) {
  console.log(`
    标题: ${item.title}
    栏目: ${item.category == "css" ? "前端" : "数据库"}
  `);
}

取数组中的最大值。

javascript
function arrayMax(array) {
  let max = array[0];
  for (const elem of array) {
    max = max > elem ? max : elem;
  }
  return max;
}

console.log(arrayMax([1, 3, 2, 9]));

迭代器方法

keys:通过迭代对象获取索引。

javascript
const arr = ["张三", "李四"];
const keys = arr.keys();
console.log(keys.next()); // {value: 0, done: false}
console.log(keys.next()); // {value: 1, done: false}
console.log(keys.next()); // {value: undefined, done: true}

使用 while 遍历。

javascript
let arr = ["张三", "李四"];
let iterator = arr.keys();
while (({ value, done } = iterator.next()) && done === false) {
  console.log(value);
}

其实在 JavaScript 中,for ... of 专门用来操作迭代对象。下面是获取数组所有键。

javascript
"use strict";
const arr = ["a", "b", "c", "d"];

for (const key of arr.keys()) {
  console.log(key);
}

values:通过迭代对象获取值。

javascript
const arr = ["a", "b", "c", "d"];
const values = arr.values();
console.log(values.next()); // {value: a, done: false}
console.log(values.next()); // {value: b, done: false}
console.log(values.next()); // {value: c, done: false}

使用 for ... of 简化书写。下面是获取数组的所有值。

javascript
"use strict";
const arr = ["a", "b", "c", "b"];

for (const value of arr.values()) {
  console.log(value);
}

entries:返回数组所有键值对,下面使用解构语法循环。

javascript
const arr = ["a", "b", "c", "d"];

console.log(arr.entries());

for (const [key, value] of arr.entries()) {
  console.log(key, value);
}

解构获取内容。

javascript
const arr = ["a", "b", "c", "d"];
const iterator = arr.entries();

/*
 * iterator.next() 得到的结果:
 *   {value: [1, 'b'], done: false}
*/
let {done,value: [k, v]} = iterator.next();

console.log(v);
[4] ES6 新特性

展开语法

展开语法(Spread syntax)允许一个可迭代的对象,如数组、字符串或对象等,能被展开在函数调用 / 构造调用、数组字面量、或对象字面量中。

4.1 数组字面量中的展开语法

使用展开语法来合并数组相比 concat 要更简单,使用 ... 可将数组展开为多个值。

javascript
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];

// 使用展开语法合并数组
const combinedArr = [...arr1, ...arr2];

console.log(combinedArr); // 输出: [1, 2, 3, 4, 5, 6]

4.2 函数调用中的展开语法

javascript
function sum(a, b, c) {
  return a + b + c;
}

const numbers = [1, 2, 3];

// 使用展开语法将数组作为参数传递给函数
console.log(sum(...numbers)); // 输出: 6

4.3 对象字面量中的展开语法

javascript
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };

// 使用展开语法合并对象
const combinedObj = { ...obj1, ...obj2 };

console.log(combinedObj); // 输出: { a: 1, b: 2, c: 3, d: 4 }

4.4 字符串中的展开语法

javascript
const str = "hello";

// 使用展开语法将字符串转换为字符数组
const chars = [...str];

console.log(chars); // 输出: ['h', 'e', 'l', 'l', 'o']

例子:节点转换

可以将 DOM 节点转为数组,下面例子不可以使用 filter 因为是节点列表。

html
<body>
  <button message="张三">button</button>
  <button message="yxts">button</button>
</body>

<script>
  let btns = document.querySelectorAll('button');
  btns.map((item) => {
    console.log(item); // Uncaught TypeError: btns.map is not a function
  })
</script>

querySelectorAll 返回的是一个 NodeList 对象,它并不是一个真正的数组,所以它并不具有数组的所有方法,比如 map。这就是为什么看到 "Uncaught TypeError: btns.map is not a function" 的错误。

解决方法一:将 NodeList 转换成真正的数组,然后再使用 map。

javascript
let btns = document.querySelectorAll('button');
btns = Array.from(btns);

解决方法二:使用 forEach 遍历 NodeList( NodeList 对象自带 forEach 方法)。

javascript
let btns = document.querySelectorAll('button');
btns.forEach((item) => {
  console.log(item);
})

解决方法三:使用展开语法后就可以使用数据方法。

html
<body>
  <div>张三</div>
  <div>李四</div>
</body>

<script>
  let divs = document.querySelectorAll("div");
  [...divs].map(function(div) {
    div.addEventListener("click", function() {
      this.classList.toggle("hide");
    });
  });
</script>

解决方法四:使用原型处理。

html
<body>
  <button message="张三">button</button>
  <button message="yxts">button</button>
</body>

<script>
  let btns = document.querySelectorAll('button');
  Array.prototype.map.call(btns, (item) => {
    item.style.background = 'red';
  });
</script>

解构赋值

解构赋值语法是一种快速提取数组或对象中的值,并将它们赋给定义的变量的简洁方式。

javascript
const colors = ['red', 'green', 'blue'];

// 传统方式获取值
const firstColor = colors[0];
const secondColor = colors[1];

// 使用解构赋值
const [a, b] = colors;
console.log(a); // 输出: red
console.log(b); // 输出: green

还可以忽略某些元素。

javascript
const colors = ['red', 'green', 'blue'];
const [a, , c] = colors;
console.log(a); // 输出: red
console.log(c); // 输出: blue

解构赋值数组 / 函数参数定义:当函数有多个返回值时,可以使用解构赋值将结果直接分配到不同的变量中。

javascript
function foo() {
  return ['张三', '李四'];
}
let [a, b] = foo();
console.log(a); // 张三

剩余解构指用一个变量来接收剩余参数。

javascript
let [a='yxts', ...b] = ['张三', '李四', 'baidu.com'];
console.log(b);

字符串解构。

javascript
"use strict";
const [...a] = "baidu.com";
console.log(a); // Array(9)

使用技巧。

javascript
function foo([a, b]) {
  console.log(a, b);
}
foo(['张三', '李四']);

交换变量的值:解构赋值可以方便地交换两个变量的值,而不需要引入临时变量。

javascript
let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a); // 输出:2
console.log(b); // 输出:1
2)Set
[1] 是什么?

用于存储任何类型的唯一值,无论是基本类型还是对象引用。

  • 只能保存值,没有键名。
  • 严格类型检测。如:字符串与数字不等于数值型数字。
  • 值是唯一的。
  • 遍历顺序是添加的顺序。
[2] 创建对象

使用数组做初始数据。

javascript
let bar = new Set(['张三', '李四']);
console.log(bar.values()); // SetIterator {"张三", "李四"}

使用字符串做初始数据。

javascript
let bar = new Set("yxts");
console.log(bar.values()); // SetIterator {"y", "x", "t", "s"}
[3] 管理元素

添加元素

Set 中是严格类型约束的,下面的数值 1 与字符串 1 属于两个不同的值。

javascript
let set = new Set();
set.add(1);
set.add("1");
console.log(set); // Set(2) {1, "1"}

使用 add 添加元素,不允许重复添加张三值。

javascript
let bar = new Set();

bar.add('李四');
bar.add('张三');
bar.add('张三')

console.log(bar.values()); // SetIterator {"李四", "张三"}

获取元素数量

javascript
let bar = new Set(['张三', '李四']);
console.log(bar.size); // 2

检测元素是否存在

javascript
let bar = new Set();
bar.add('张三');
console.log(bar.has('张三')); // true

删除元素

使用 delete 方法删除单个元素,返回值为 boolean 类型。

javascript
let bar = new Set();
bar.add("张三");
bar.add("李四");

console.log(bar.delete("张三")); // true

console.log(bar.values());
console.log(bar.has("张三")); // false

使用 clear 删除所有元素。

javascript
let bar = new Set();
bar.add("张三");
bar.add("李四");
bar.clear();
console.log(bar.values());

数组与 set 互相转换

可以使用点语法或 Array.form() 静态方法将 Set 类型转为数组,这样就可以使用数组方法处理 set 了。

javascript
const set = new Set(['张三', '李四']);

console.log([...set]); // ["张三", "李四"]
console.log(Array.from(set)); // ["张三", "李四"]

移除 Set 中大于 5 的数值。

javascript
let bar = new Set("123456789");
bar = new Set([...bar].filter(item => item < 5));
console.log(bar);

去除字符串重复。

javascript
console.log([...new Set("526546")].join("")); // 5264

去除数组重复。

javascript
const arr = [1, 2, 3, 5, 2, 3];
console.log(...new Set(arr)); // 1,2,4,5

取交集:获取两个集合中共同存在的元素。

javascript
let bar = new Set(['张三', '李四']);
let aaa = new Set(['王五', '李四']);
let newSet = new Set(
	[...bar].filter(item => aaa.has(item))
);
console.log(newSet); // {"李四"}

在集合 a 中出现但不在集合 b 中出现元素集合

javascript
let bar = new Set(['张三', '李四']);
let aaa = new Set(['王五', '李四']);
let newSet = new Set(
	[...bar].filter(item => !aaa.has(item))
);
console.log(newSet); // {"张三"}

并集:将两个集合合并成一个新的集合,由于 Set 特性当然也不会产生重复元素。

javascript
let bar = new Set(['张三', '李四']);
let aaa = new Set(['王五', '李四']);
let newSet = new Set([...bar, ...aaa]);
console.log(newSet);
[4] 遍历数据

使用 keys()/values()/entries() 都可以返回迭代对象。因为 set 类型只有值,所以 keys 与 values 方法结果一致。

javascript
const bar = new Set(["张三", "李四"]);
console.log(bar.values()); // SetIterator {"张三", "李四"}
console.log(bar.keys()); // SetIterator {"张三", "李四"}
console.log(bar.entries()); // SetIterator {"张三" => "张三", "李四" => "李四"}

可以使用 forEach 遍历 Set 数据,默认使用 values 方法创建迭代器。

为了保持和遍历数组参数统一,函数中的 value 与 key 是一样的。

javascript
let arr = [7, 6, 2, 8, 2, 6];
let set = new Set(arr);
// 使用 forEach 遍历
set.forEach((item,key) => console.log(item,key));

也可以使用 for...of 遍历 Set 数据,默认使用 values 方法创建迭代器。

javascript
// 使用 for...of 遍历
let set = new Set([7, 6, 2, 8, 2, 6]);

for (const v of set) {
  console.log(v);
}

来看个例子。

html
<body>
  <input type="text">
  <ul></ul>
</body>

<script>
  let obj = {
    words: new Set(),
    set keyword(word) {
      this.words.add(word);
    },
    show() {
      let ul = document.querySelector('ul');
      ul.innerHTML = '';
      this.words.forEach((item) => {
        ul.innerHTML += ('<li>' + item + '</li>');
      })
    }
  }

  document.querySelector('input').addEventListener('blur', function () {
    obj.keyword = this.value;
    obj.show();
  });
</script>
[5] WeakSet

WeakSet 结构同样不会存储重复的值,它的成员必须只能是对象类型的值。

  • 垃圾回收不考虑 WeakSet,即被 WeakSet 引用时,引用计数器不加一,所以对象不被引用时不管 WeakSet 是否在使用都将删除。
  • 因为 WeakSet 是弱引用,由于其他地方操作成员可能会不存在,所以不可以进行 forEach() 遍历等操作。
  • 也是因为弱引用,WeakSet 结构没有 keys( ), values( ), entries( ), for...of 等方法和 size 属性。
  • 因为是弱引用所以当外部引用删除时,希望自动删除数据时使用 WeakMap。

声明定义

以下操作由于数据不是对象类型将产生错误。

javascript
new WeakSet(["张三", "李四"]); // Invalid value used in weak set

new WeakSet("张三"); // Invalid value used in weak set

WeakSet 的值必须为对象类型。

javascript
new WeakSet([["张三"], ["李四"]]);

将 DOM 节点保存到 WeakSet。

javascript
document.querySelectorAll("button").forEach(item => Wset.add(item));

基本操作

下面是 WeakSet 的常用指令。

javascript
const bar = new WeakSet();
const arr = ["张三"];
// 添加操作
bar.add(arr);
console.log(bar.has(arr));

// 删除操作
bar.delete(arr);

// 检索判断
console.log(bar.has(arr));

案例

html
<body>
  <ul>
    <li>xxx.com <a href="javascript:;">x</a></li>
    <li>baidu.com <a href="javascript:;">x</a></li>
    <li>jd.com <a href="javascript:;">x</a></li>
  </ul>
</body>

<script>
  class Todos {
    constructor() {}
    run() {
      this.items = document.querySelectorAll("ul>li");
      this.lists = new WeakSet();
      this.record();
      this.addEvent();
    }
    record() {
      this.items.forEach(item => this.lists.add(item));
    }
    addEvent() {
      this.items.forEach(item => {
        item.querySelector("a").addEventListener("click", event => {
          // 检测 WeakSet 中是否存在 Li 元素
          const parentElement = event.target.parentElement;
          if (!this.lists.has(parentElement)) {
            alert("已经删除此 TODO");
          } else {
            // 删除后从记录的 WakeSet 中移除
            parentElement.remove();
            this.lists.delete(parentElement); // 此步可选,因为当被垃圾回收时,WeakSet自动清理,避免内存泄漏
          }
        });
      });
    }
  }
  new Todos().run();
</script>
3)Map
[1] 是什么?

Map 是一组键值对的结构,用于解决以往不能用对象做为键的问题。

  • 具有极快的查找速度。
  • 函数、对象、基本类型都可以作为键或值。
[2] 声明定义

可以接受一个数组作为参数,该数组的成员是一个表示键值对的数组。

javascript
let m = new Map([
  ['1号', '张三'],
  ['2号', '李四']
]);

console.log(m.get('1号')); // 张三

使用 set 方法添加元素,支持链式操作。

javascript
let map = new Map();
let obj = {
  name: "张三"
};

map.set(obj, "JavaScript").set("name", "李四");

console.log(map.entries()); // MapIterator {{…} => "JavaScript", "name" => "李四"}

使用构造函数 new Map 创建的原理如下。

javascript
const bar = new Map();
const arr = [["1号", "张三"], ["2号", "王五"]];

arr.forEach(([key, value]) => {
  bar.set(key, value);
});
console.log(bar); // Map(2) { '1号' => '张三', '2号' => '王五' }

对于键是对象的 Map, 键保存的是内存地址,值相同但内存地址不同的视为两个键。

javascript
let arr = ["张三"];
const bar = new Map();

bar.set(arr, "JavaScript");
console.log(bar.get(arr)); // JavaScript
console.log(bar.get(["张三"])); // undefined
[3] 管理元素

获取数据数量

javascript
let map = new Map(["1号","张三"],["2号","王五"]);
console.log(map.size);

检测元素是否存在

javascript
let map = new Map();

let obj = {
  name: '张三'
}
map.set(obj, '学习 JavaScript');

console.log(map.has(obj));

读取元素

javascript
let map = new Map();

let obj = {
  name: '张三'
}

map.set(obj, '学习 JavaScript');
console.log(map.get(obj));

删除元素

使用 delete() 方法删除单个元素。

javascript
let map = new Map();
let obj = {
	name: '张三'
}

map.set(obj, 'xxx.com');
console.log(map.get(obj));

map.delete(obj);
console.log(map.get(obj));

使用 clear() 方法清除 Map 所有元素。

javascript
let map = new Map();
let obj1 = {
  name: '张三'
}

let obj2 = {
  name: '李四'
}

map.set(obj1, {
  title: '内容管理系统'
});

map.set(obj2, {
  title: '智慧 AI 系统'
});

console.log(map.size);
console.log(map.clear());
console.log(map.size);

数组转换

可以使用展开语法或 Array.form() 静态方法将 map 类型转为数组,这样就可以使用数组方法处理了。

javascript
let bar = new Map([["1号", "张三"], ["2号", "李四"]]);

console.log(...bar); // (2) ["1号", "张三"] (2) ["2号", "李四"]
console.log(...bar.entries()); // (2) ["1号", "张三"] (2) ["2号", "李四"]
console.log(...bar.values()); // 张三 李四
console.log(...bar.keys()); // 1号 2号

检索包含李四的值组成新 Map。

javascript
let bar = new Map([["1号", "张三"], ["2号", "李四"]]);

let newArr = [...bar].filter(function(item) {
  return item[1].includes("李四");
});

bar = new Map(newArr);
console.log(...bar.keys());
[4] 遍历数据

使用 keys()/values()/entries() 都可以返回可遍历的迭代对象。

javascript
let bar = new Map([["1号", "张三"], ["2号", "李四"]]);
console.log(bar.keys()); // MapIterator {"1号", "2号"}
console.log(bar.values()); // MapIterator {"张三", "李四"}
console.log(bar.entries()); // MapIterator {"1号" => "张三", "2号" => "李四"}

可以使用 keys/values 函数遍历键与值。

javascript
let bar = new Map([["1号", "张三"], ["2号", "李四"]]);
for (const key of bar.keys()) {
  console.log(key);
}
for (const value of bar.values()) {
  console.log(value);
}

使用 for...of 遍历操作,直播遍历 Map 等同于使用 entries() 函数。

javascript
let bar = new Map([["1号", "张三"], ["2号", "李四"]]);
for (const [key, value] of bar) {
  console.log(`${key}=>${value}`);
}

使用 forEach 遍历操作。

javascript
let bar = new Map([["1号", "张三"], ["2号", "李四"]]);
bar.forEach((value, key) => {
  console.log(`${key}=>${value}`);
});

例子

例一:节点集合

map 的 key 可以为任意类型,下面使用 DOM 节点做为键来记录数据。

html
<body>
  <div desc="我来啦">雨下田上</div>
  <div desc="欢迎回来">兜兜风</div>
</body>

<script>
  const divMap = new Map();
  const divs = document.querySelectorAll("div");

  divs.forEach(div => {
    divMap.set(div, {
      content: div.getAttribute("desc")
    });
  });

  divMap.forEach((config, elem) => {
    elem.addEventListener("click", function() {
      alert(divMap.get(this).content);
    });
  });
</script>

例子二:同意协议

当不接受协议时无法提交表单,并根据自定义信息提示用户。

html
<form action="" onsubmit="return post()">
  接受协议:
  <input type="checkbox" name="agreement" message="请接受接受协议" />
  我是学生:
  <input type="checkbox" name="student" message="网站只对学生开放" />
  <input type="submit" />
</form>

<script>
  function post() {
    let map = new Map();

    let inputs = document.querySelectorAll("[message]");
    // 使用 set 设置数据
    inputs.forEach(item =>
      map.set(item, {
        message: item.getAttribute("message"),
        status: item.checked
      })
    );

    // 遍历 Map 数据
    return [...map].every(([item, config]) => {
      config.status || alert(config.message);
      return config.status;
    });
  }
</script>
[5] WeakMap

WeakMap 对象是一组键 / 值对的集。

  • 键名必须是对象

  • WeaMap 对键名是弱引用的,键值是正常引用。

  • 垃圾回收不考虑 WeaMap 的键名,不会改变引用计数器,键在其他地方不被引用时即删除。

  • 因为 WeakMap 是弱引用,由于其他地方操作成员可能会不存在,所以不可以进行 forEach() 遍历等操作。

  • 也是因为弱引用,WeaMap 结构没有 keys( ),values( ),entries( ) 等方法和 size 属性。

  • 当键的外部引用删除时,希望自动删除数据时使用 WeakMap。

声明定义

以下操作由于键不是对象类型将产生错误。

javascript
new WeakMap("JavaScript"); // TypeError: Invalid value used in weak set

将 DOM 节点保存到 WeakMap。

html
<body>
  <div>JavaScript</div>
  <div>Vue</div>
</body>

<script>
  const bar = new WeakMap();
  document
    .querySelectorAll("div")
    .forEach(item => bar.set(item, item.innerHTML));
  console.log(bar); // WeakMap {div => "JavaScript", div => "Vue"}
</script>

基本操作

下面是 WeakSet 的常用指令。

javascript
const bar = new WeakMap();
const arr = ["JavaScript"];
// 添加操作
bar.set(arr, "努力学习中……");
console.log(bar.has(arr)); // true

// 删除操作
bar.delete(arr);

// 检索判断
console.log(bar.has(arr)); // false

垃圾回收

WakeMap 的键名对象不会增加引用计数器,如果一个对象不被引用了会自动删除。

  • 下例当 bar 删除时,内存即清除,因为 WeakMap 是弱引用不会产生引用计数。
  • 当垃圾回收时,因为对象被删除,这时 WakeMap 也就没有记录了。
javascript
let map = new WeakMap();
let bar = {};
map.set(bar, "JavaScript");
bar = null;
console.log(map);

setTimeout(() => {
  console.log(map);
}, 1000);

例子

html
<body>
  <div>
    <ul>
      <li><span>php</span> <a href="javascript:;">+</a></li>
      <li><span>js</span> <a href="javascript:;">+</a></li>
      <li><span>java</span> <a href="javascript:;">+</a></li>
    </ul>
  </div>
  <div>
    <strong id="count">共选了2门课</strong>
    <p id="lists"></p>
  </div>
</body>

<script>
  class Lesson {
    constructor() {
      this.lis = document.querySelectorAll("ul>li");
      this.countELem = document.getElementById("count");
      this.listElem = document.getElementById("lists");
      this.map = new WeakMap();
    }
    run() {
      this.lis.forEach(item => {
        item.querySelector("a").addEventListener("click", event => {
          const elem = event.target;
          const state = elem.getAttribute("select");
          if (state) {
            elem.removeAttribute("select");
            this.map.delete(elem.parentElement);
            elem.innerHTML = "+";
            elem.style.backgroundColor = "green";
          } else {
            elem.setAttribute("select", true);
            this.map.set(elem.parentElement, true);
            elem.innerHTML = "-";
            elem.style.backgroundColor = "red";
          }
          this.render();
        });
      });
    }
    count() {
      return [...this.lis].reduce((count, item) => {
        return (count += this.map.has(item) ? 1 : 0);
      }, 0);
    }
    lists() {
      return [...this.lis]
        .filter(item => {
          return this.map.has(item);
        })
        .map(item => {
          return `<span>${item.querySelector("span").innerHTML}</span><br>`;
        });
    }
    render() {
      this.countELem.innerHTML = `共选了${this.count()}课`;
      this.listElem.innerHTML = this.lists().join("");
    }
  }
  new Lesson().run();
</script>

四、运算符

1. 算数运算符

运算符说明
+加法
-减法
*乘法
/除法
%取余数
++自增
--自减
**求幂

2. 比较运算符

运算符说明
>大于
<小于
==会进行隐式类型转换
>=大于或等于
<=小于等于
!=不相等
===不做类型转换
!==严格不相等

3. 逻辑运算符

运算符说明
&&如果第一个表达式为假,直接返回假值,不再计算第二个表达式(短路求值)。
||如果第一个表达式为真,直接返回真值,不再计算第二个表达式(短路求值)。
!表示逻辑非,即原来是 true 转变为 false,反之亦然。

注意:&& 的优先级高,所以结果是 true。

javascript
console.log(true || false && false);
// 先计算 false && false -> false
// 在计算 true || false -> true
// 结果为 true

可以使用 () 来提高优先级。

javascript
console.log((true || false) && false); // false

4. 赋值运算符

1) 基本赋值运算符

使用 = 进行变量赋值。

2) 复合赋值运算符

可以使用 +=、-=、*=、/=、%= 简写算术运算。即 n*=2 等同于 n=n*2

5. 三目运算符

javascript
条件表达式 ? 为真执行的语句 : 为假执行的语句;

6. 位运算符

  • 按位与(&):对两个操作数的每一位执行与操作。
  • 按位或(|):对两个操作数的每一位执行或操作。
  • 按位异或(^):对两个操作数的每一位执行异或操作。
  • 按位非(~):对操作数的每一位取反。
  • 左移(<<):将左操作数的二进制表示向左移动指定的位数,右侧用 0 填充。
  • 有符号右移(>>):将左操作数的二进制表示向右移动指定的位数,保留符号位(原数字是正数,高位补 0;原数字是负数,高位补 1)。
  • 无符号右移(>>>):将左操作数的二进制表示向右移动指定的位数,左侧用 0 填充。
例子:使用异或运算符交换变量

异或运算的几个关键特性
1. 交换律:a ^ b = b ^ a
2. 结合律:(a ^ b) ^ c = a ^ (b ^ c)
3. 自反性:a ^ a = 0
4. 与0异或:a ^ 0 = a

交换变量
let x = 5;
let y = 10;

交换步骤
1. x = x ^ y
   此时,x 的值变成了 x 和 y 的异或结果。我们可以将这个新的 x 表示为 (x ^ y)。
   y 保持不变。
2. y = x ^ y
   这一步实际上是:y = (x ^ y) ^ y
   根据结合律:(x ^ y) ^ y = x ^ (y ^ y)
   由于 y ^ y = 0,所以这等价于 x ^ 0,而 x ^ 0 = x
   因此,这一步后 y 的值变成了原来 x 的值。
3. x = x ^ y
   现在 x 还是 (x ^ y),y 已经是原来的 x 值了。
   所以这一步实际上是:x = (x ^ y) ^ x
   again,(x ^ y) ^ x = y ^ (x ^ x) = y ^ 0 = y

7. 类型检测

typeof

typeof 用于返回以下原始类型。

  • 基本类型:number / boolean / string
  • undefined
  • function
  • object
  • symbol
  • bigint

可以使用 typeof 用于判断数据的类型。

javascript
// 未赋值或不存在的变量返回 undefined
var bar;
console.log(typeof bar); // undefined
console.log(typeof yxts); // undefined

function run() {}
console.log(typeof run); // function

let c = [1, 2, 3];
console.log(typeof c); // object

let d = { name: "https://baidu.com" };
console.log(typeof d); // object

instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

也可以理解为是否为某个对象的实例,typeof 不能区分数组,但 instanceof 则可以。

后面章节会详细介绍原型链。

javascript
let bar = [];
let xiaowang = {};
console.log(bar instanceof Array); // true
console.log(xiaowang instanceof Array); // false

let d = { name: "javascript" };
console.log(d instanceof Object); // true

function User() {}
let stu = new User();
console.log(stu instanceof User); // true

五、流程控制

1. 条件语句

  • if...else
  • switch

2. 循环语句

  • for
  • while
  • do...while
  • for...in
  • for...of

3. 跳转语句

  • break
  • continue
  • return

第二章:函数

学习如何定义和调用函数,理解函数的参数和返回值,以及高阶函数和闭包、作用域、作用域链等概念。

一、函数的声明

1. 普通函数

在 JS 中,函数也是对象,函数是 Function 类创建的实例,下面的例子可以方便理解函数是对象。

javascript
let foo = new Function("title", "console.log(title)");
foo('张三');

标准语法是使用函数声明来定义函数。

javascript
function foo(num) {
  return ++num;
}
console.log(foo(3)); // 4

也可以函数表达式声明。

javascript
var myFunction = function() {
  console.log('Hello, World!');
};

注意:函数没有返回值或者写了 return 语句,但后面没有任何值,都是默认返回 undefined。

上面三种方式声明的函数有什么区别?

全局函数会声明在 window 对象中。例如:window.screenX 是一个只读属性,返回浏览器窗口左上角的水平坐标(以像素为单位)。这个坐标是相对于用户屏幕左上角的。

javascript
console.log(window.screenX); // 2200

当我们定义了 screenX 函数后就覆盖了 window.screenX 属性。

javascript
function screenX() {
  return "张三";
}
console.log(window.screenX); // 这将输出名为 screenX 的函数,而不是原始的 window.screenX 属性。

此外,还有使用 var 时会压入 window。

javascript
var foo = function() {
  console.log("张三");
};
window.foo(); // 张三

使用 let / const 时不会压入 window。

javascript
let foo = function() {
  console.log("张三");
};
window.foo(); // window.foo is not a function

2. 立即执行函数 IIFE

立即执行函数指函数定义时立即执行。

定义方式

javascript
// 1.使用圆括号包裹函数表达式
(function() {
  // 代码
})();

// 2.使用圆括号包裹函数表达式,并在函数表达式后面再加一个圆括号
(function() {
  // 代码
}());

// 3.使用箭头函数
(() => {
  // 代码
})();

// 4.在函数表达式前面加一个+、-、!或~等一元运算符
/**
 * 在JavaScript中,函数声明不能直接被调用,但函数表达式可以。
 * 为了将函数声明转换为函数表达式,我们可以在函数声明前面添加一元运算符,如+、-、!或~等。
 */
+function() {
  // 代码
}();

!function() {
  // 代码
}();

应用场景

立即执行函数有自己的作用域。

javascript
"use strict";
(function () {
  var web = '张三';
})();

console.log(web); // web is not defined

利用这个特性,可以用来定义私有作用域防止污染全局作用域。

javascript
"use strict";
(function () {
  var web = '张三';
  function show(){
    console.log("hello");
  }

  window.js1 = { web, show }
})();

console.log(window.js1.web); // 张三

现在,可以使用 ES6 的 let/const 有块作用域特性,来产生私有作用域。

javascript
{
  let web = '张三';

  function show(){
    console.log("hello");
  }

  window.js1 = { web, show }
}

console.log(window.js1.web); // 张三

二、this 关键字

1. 绑定规则

javascript
// 定义一个函数
function foo() {
  console.log(this);
}

// 1.调用方式一:直接调用
foo(); // window

// 2.调用方式二:将foo放到一个对象中, 再调用
var obj = {
  name: "why",
  foo: foo
}

obj.foo(); // obj对象

// 3.调用方式三:通过call/apply调用
foo.call("abc"); // String {"abc"}对象

通过这个案例可以给我们什么样的启示呢?

  • 函数在调用时,JavaScript 会默认给 this 绑定一个值。
  • this 的绑定跟定义的位置(编写的位置)没有关系。
  • this 的绑定跟调用方式以及调用的位置有关系。
  • 因此,this 是在运行时被绑定的。
[1] 默认绑定

独立函数调用就是默认绑定。

全局环境下 this 就是 window 对象的引用。

html
<script>
  console.log(this == window); // true
</script>

使用严格模式时在全局函数内 this 为 undefined。

javascript
var bar = '张三';
function get() {
  "use strict"
  return this.bar;
}
console.log(get()); // 严格模式将产生错误 TypeError: Cannot read properties of undefined (reading 'bar')

这种也是独立函数调用。

javascript
function foo() {
  console.log(this)
}

var obj1 = {
  name: 'obj1',
  foo: foo
}

var bar = obj1.foo
bar() // window

这种情况下 foo 也是。

javascript
let obj = {
  site: "张三",
  show() {
    console.log(this.site); // 张三
    console.log(`this in show method: ${this}`); // this in show method: [object Object]
    function foo() {
      console.log(typeof this.site); // undefined
      console.log(`this in foo function: ${this}`); // this in foo function: [object Window]
    }
    foo();
  }
};
obj.show();

下面这种代码比较诡异,但面试高频。

创建一个函数的间接引用,这种情况使用默认绑定规则。赋值 (obj2.foo = obj1.foo) 的结果是 foo 函数。

foo 函数被直接调用,那么是默认绑定。

javascript
function foo() {
  console.log(this);
}

var obj1 = {
  name: "obj1",
  foo: foo
};

var obj2 = {
  name: "obj2"
};

obj1.foo(); // 输出: obj1对象

(obj2.foo = obj1.foo)(); // 输出: window (在浏览器环境中)
[2] 隐式绑定

方法调用就是隐式绑定。

函数为对象的方法时,this 指向该对象。

[3] new 绑定

JavaScript 中的函数可以当做一个类的构造函数来使用,也就是使用 new 关键字。

javascript
function User() {
  this.name = "张三";
  this.say = function() {
    console.log(this); // User {name: "张三", say: ƒ}
    return this.name;
  };
}

let bar = new User();
console.log(bar.say()); // 张三
[4] 显示绑定

"显示绑定"是指通过使用 .call().apply().bind() 方法明确地将 this 绑定到某个函数上。

  • .apply(context, [arg1, arg2, ...]):这个方法的作用和 .call() 类似,但是它接受一个参数数组,而不是一系列的参数。
  • .call(context, arg1, arg2, ...):这个方法接受一个新的 this 上下文和一系列参数,然后立即执行函数。
  • .bind(context, arg1, arg2, ...):这个方法返回一个新的函数,this 被绑定到指定的上下文。这个新的函数可以稍后被调用。
[5] 规则优先级

由低到高分别是:

new 绑定 ➡ bind ➡ apply / call ➡ 隐式绑定 ➡ 默认绑定

注意

① new 绑定和 call、apply 是不允许同时使用的,所以不存在谁的优先级更高。

② new 绑定可以和 bind 一起使用,new 绑定优先级更高。例如:

javascript
function Foo(name) {
 this.name = name;
}
var obj = { name: 'obj' };

var BoundFoo = Foo.bind(obj);
var instance = new BoundFoo('instance');
console.log(instance.name); // 'instance'
console.log(obj.name); // 'obj'

2. 面试题

[1] 面试题一
javascript
var name = "window";

var person = {
  name: "person",
  sayName: function () {
    console.log(this.name);
  }
};

function sayName() {
  var sss = person.sayName;
  sss();
  person.sayName();
  (person.sayName)(); // 不要被小括号迷惑, "."的优先级很高的, 他依然是隐式绑定, this指向发起调用的对象person
                      // 只是用括号进行分组,保持了对象引用关系
  (b = person.sayName)(); // 涉及赋值操作,打破了对象引用关系
}

sayName();
点我查看答案
    默认绑定, window -> window
    隐式绑定, person -> person
    隐式绑定, person -> person
    间接函数引用, window -> window
  
[2] 面试题二
javascript
var name = 'window'
var person1 = {
  name: 'person1',
  foo1: function () {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },
  foo4: function () {
    return () => {
      console.log(this.name)
    }
  }
}

var person2 = { name: 'person2' }

person1.foo1();
person1.foo1.call(person2);

person1.foo2();
person1.foo2.call(person2);

person1.foo3()();
person1.foo3.call(person2)();
person1.foo3().call(person2);

person1.foo4()();
person1.foo4.call(person2)();
person1.foo4().call(person2);
点我查看答案
    隐式绑定: person1
    显式绑定: person2
    上层作用域: window
    上层作用域: window
    默认绑定: window
    默认绑定: window
    显式绑定: person2
    person1
    person2
    person1
  
[3] 面试题三
javascript
var name = 'window'
function Person (name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
  },
  this.foo2 = () => console.log(this.name),
  this.foo3 = function () {
    return function () {
      console.log(this.name)
    }
  },
  this.foo4 = function () {
    return () => {
      console.log(this.name)
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

person1.foo1()
person1.foo1.call(person2)

person1.foo2()
person1.foo2.call(person2)

person1.foo3()()
person1.foo3.call(person2)()
person1.foo3().call(person2)

person1.foo4()()
person1.foo4.call(person2)()
person1.foo4().call(person2)
点我查看答案
    隐式绑定: person1
    显式绑定: person2
    上层作用域查找: person1
    上层作用域查找: person1
    默认绑定: window
    默认绑定: window
    显式绑定: person2
    上层作用域查找: person1 (隐式绑定)
    上层作用域查找: person2 (显式绑定)
    上层作用域查找: person1 (隐式绑定)
  
[4] 面试题四
javascript
var name = 'window'
function Person (name) {
  this.name = name
  this.obj = {
    name: 'obj',
    foo1: function () {
      return function () {
        console.log(this.name)
      }
    },
    foo2: function () {
      return () => {
        console.log(this.name)
      }
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

person1.obj.foo1()()
person1.obj.foo1.call(person2)()
person1.obj.foo1().call(person2)

person1.obj.foo2()()
person1.obj.foo2.call(person2)()
person1.obj.foo2().call(person2)
点我查看答案
    默认绑定: window
    默认绑定: window
    显式绑定: person2
    上层作用域查找: obj (隐式绑定)
    上层作用域查找: person2 (显式绑定)
    上层作用域查找: obj (隐式绑定)
  

三、函数高级

1. 函数对象的属性

在 js 中,函数既是函数(包括构造函数),也是对象。

  • name 属性:获取函数的名字,可以通过 name 来访问;

  • length 属性:用于返回函数形参的个数;

    注意:rest 参数(剩余参数)是不参与参数的个数的;函数形参有默认值的,也不参与参数的个数。

2. 函数的参数收集

需要知道,每个函数中都有一个默认的参数 arguments,arguments 对象是早期 JavaScript 版本中的遗留特性。在ES6(也称为ES2015)中,引入了剩余参数(...)语法,这提供了更清晰、更现代的方法来处理未知数量的函数参数。

javascript
function exampleFunction(...args) {
  console.log(args); // 这是一个真正的数组
}

exampleFunction(1, 2, 3, 4); // 输出: [1, 2, 3, 4]

arguments 是一个对应于传递给函数的参数的类数组 (array-like) 对象。所以它没有数组的方法,例如 forEach、filter、map 等方法。

如何把 arguments 转 Array?

方法一:使用普通 for 循环或者 for ... of 遍历出每一项,把它添加到新创建的数组中。

方法二:使用 Array.from(ES6)

javascript
function myFunction() {
  var args = Array.from(arguments);
  args.forEach(function(arg) {
    console.log(arg);
  });
}

方法三:使用扩展运算符(ES6)

javascript
function myFunction() {
  var args = [...arguments];
  args.forEach(function(arg) {
    console.log(arg);
  });
}

方法四:使用数组的 slice 方法

javascript
function myFunction() {
  var args = Array.prototype.slice.call(arguments);
  args.forEach(function(arg) {
    console.log(arg);
  });
}

Array.prototype.slice.call(arguments); 也可以使用 [].slice.call(arguments);

注意:
箭头函数不绑定 arguments。所以在箭头函数中使用 arguments 会去上层作用域查找。
箭头函数不绑定 this。所以在箭头函数中使用 this 会去上层作用域查找。

剩余参数和 arguments 有什么区别呢?

  • 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参 arguments。
  • 对象不是一个真正的数组,而 rest 参数是一个真正的数组,可以进行数组的所有操作。
  • arguments 是早期的 ECMAScript 中为了方便去获取所有的参数提供的一个数据结构,而 rest 参数是 ES6 中提供,并且希望以此来替代 arguments 的。

3. 其他类型函数

1)高阶函数

高阶函数是一种可以接收其他函数作为参数,或者返回一个函数作为结果的函数。

2)纯函数

函数式编程中有一个非常重要的概念叫纯函数。JavaScript 符合函数式编程的范式,所以也有纯函数的概念。

纯函数的维基百科定义:

在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数。

  • 此函数在相同的输入值时,需产生相同的输出。
  • 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由 I/O 设备产生的外部输出无关。
  • 该函数不能有语义上可观察的函数副作用,诸如 “触发事件“ 使输出设备输出,或更改输出值以外物件的内容等。

当然上面的定义会过于的晦涩,所以简单总结一下:

  • 确定的输入,一定会产生确定的输出。
  • 函数在执行过程中,不能产生副作用。

副作用是什么?

在计算机科学中,引用了副作用的概念,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响。比如修改了全局变量、修改参数或者改变外部的存储。

3)柯里化函数

概念

柯里化也是属于函数式编程里面一个非常重要的概念。是一种关于函数的高阶技术;它不仅被用于 JavaScript,还被用于其他编程语言。

先来看一下维基百科的解释:

在计算机科学中,柯里化(Currying),又译为卡瑞化或加里化。是把接收多个参数的函数,变成接受一个单一参数 (最初函数的第一个参数)的函数,并且返回接受余下的参数,而且返回结果的新函数的技术。

柯里化声称 “如果你固定某些参数,你将得到接受余下参数的一个函数”。

维基百科的解释非常的抽象,这里做一个总结:

  • 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数。这个过程就称之为柯里化。
  • 柯里化是一种函数的转换,将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)
  • 柯里化不会调用函数。它只是对函数进行转换。

自动柯里化函数的实现

javascript
function autoCurry(fn, paramLen = fn.length) {
  return function curried(...args) {
    if (args.length >= paramLen) {
      return fn.apply(null, args);
    } else {
      return function(...args2) {
        return curried.apply(null, args.concat(args2));
      }
    }
  };
}

// 使用示例
const sum = (a, b, c) => a + b + c;
const curriedSum = autoCurry(sum);

console.log(curriedSum(1)(2)(3)); // 输出:6
console.log(curriedSum(1, 2)(3)); // 输出:6
console.log(curriedSum(1, 2, 3)); // 输出:6

四、内存管理和闭包

1. 内存管理 / 预编译原理

探讨的是函数执行过程。

1)专业术语

作用域

作用域是指程序源代码中定义变量的区域。规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

JavaScript 采用词法作用域,也就是静态作用域。

什么是静态作用域与动态作用域?

因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。

备注:bash 是动态作用域。

js 产生作用域的代码有哪些?全局作用域、函数作用域、块级作用域(流控语句 + 单独的花括号)。

可执行代码

JavaScript 的可执行代码的类型有哪些?

就三种,全局代码、函数代码、eval 代码。

VO 对象(Variable Object)

每一个执行上下文会关联一个 VO(Variable Object,变量对象),变量和函数声明会被添加到这个 VO 对象中。

当全局代码被执行的时候,VO 就是 GO 对象了。当函数执行时,VO 就是 AO。

全局对象 GO

预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。

在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。因为全局对象是作用域链的头,这意味着所有非限定性的变量和函数名都会作为该对象的属性来查询。

例如,当 JavaScript 代码引用 parseInt() 函数时,它引用的是全局对象的 parseInt 属性。全局对象是作用域链的头,还意味着在顶层 JavaScript 代码中声明的所有变量都将成为全局对象的属性。

备注:在 js 中,全局对象是 window。

活动对象 AO

活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。

执行上下文

js 引擎内部有一个执行上下文栈(Execution Context Stack,简称 ECS),它是用于执行代码的调用栈。

那么现在它要执行谁呢?执行的是全局的代码块。

全局的代码块为了执行会构建一个 Global Execution Context(GEC)。GEC 会被放入到 ECS 中执行。

GEC 被放入到 ECS 中里面包含两部分内容:

第一部分:在代码执行前,在 parser 转成 AST 的过程中,会将全局定义的变量、函数等加入到 Global Object 中,但是并不会赋值。这个过程也称之为变量的作用域提升(hoisting)。

第二部分:在代码执行中,对变量赋值,或者执行其他的函数。


执行上下文的代码会分成两个阶段进行处理:分析和执行

  1. 进入执行上下文
  2. 代码执行

进入执行上下文

当进入执行上下文时,这时候还没有执行代码。

变量对象会包括:

  1. 函数的所有形参 (如果是函数上下文)

    • 由名称和对应值组成的一个变量对象的属性被创建
    • 没有实参,属性值设为 undefined
  2. 函数声明

    • 由名称和对应值(函数对象)组成一个变量对象的属性被创建
    • 如果变量对象已经存在相同名称的属性,则完全替换这个属性
  3. 变量声明

    • 由名称和对应值组成一个变量对象的属性被创建;

    • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

      创建阶段和执行阶段。

      在创建阶段,JavaScript 引擎会先找到所有的变量声明和函数声明,然后在内存中为它们分配空间。这个过程被称为 "变量提升"(hoisting)。在这个阶段,函数声明会被完全定义(包括函数名和函数体),但是变量只会被声明,不会被初始化(即,变量的值是 undefined)。

      如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。这是因为函数声明的优先级高于变量声明。如果一个变量和一个函数同名,那么在创建阶段,函数声明会覆盖变量声明。

      然后,在执行阶段,当代码开始逐行执行时,变量会被实际的值初始化。如果一个变量和一个函数同名,那么在执行阶段,变量的值会覆盖函数。

      javascript
      var bar = 10
      console.log(bar) // 10
      function bar(){
      }
      console.log(bar) // 10
      javascript
      console.log(foo); // 输出: function foo() { console.log("second"); }
      
      var foo = "bar";
      
      function foo() {
            console.log("first");
      }
      
      function foo() {
            console.log("second");
      }
      
      console.log(foo); // 输出: "bar"

举个例子:

javascript
function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};

  b = 3;
}

foo(1);

在进入执行上下文后,这时候的 AO 是:

json
AO = {
  arguments: {
    0: 1,
    length: 1
  },
  a: 1,
  c: reference to function c(){},
  b: undefined,
  d: undefined
}

代码执行

在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值。

还是上面的例子,当代码执行完后,这时候的 AO 是:

json
AO = {
  arguments: {
    0: 1,
    length: 1
  },
  a: 1,
  c: reference to function c(){},
  b: 3,
  d: reference to FunctionExpression "d"
}

到这里变量对象的创建过程就介绍完了,让我们简洁的总结我们上述所说:

① 全局上下文的变量对象初始化是全局对象。

② 函数上下文的变量对象初始化只包括 Arguments 对象。

③ 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值。

④ 在代码执行阶段,会再次修改变量对象的属性值。

作用域链(Scope Chain)

当进入到一个执行上下文时,执行上下文也会关联一个作用域链。

作用域链是一个对象列表,用于变量标识符的求值。

当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象。

2)完整执行过程

函数执行上下文中作用域链和变量对象的创建过程:

javascript
var scope = "global scope";
function checkscope(){
  var scope2 = 'local scope';
  return scope2;
}
checkscope();

执行过程如下:

① checkscope 函数被创建,保存作用域链到内部属性 [[scope]]

javascript
checkscope.[[scope]] = [
  globalContext.VO
];

② 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈

javascript
ECStack = [
  checkscopeContext,
  globalContext
];

③ checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数 [[scope]] 属性创建作用域链

javascript
checkscopeContext = {
  Scope: checkscope.[[scope]],
}

④ 第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明

javascript
checkscopeContext = {
  AO: {
    arguments: {
      length: 0
    },
    scope2: undefined
  },
  Scope: checkscope.[[scope]],
}

⑤ 第三步:将活动对象压入 checkscope 作用域链顶端

javascript
checkscopeContext = {
  AO: {
    arguments: {
      length: 0
    },
    scope2: undefined
  },
  Scope: [AO, [[Scope]]]
}

⑥ 准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值

javascript
checkscopeContext = {
  AO: {
    arguments: {
      length: 0
    },
    scope2: 'local scope'
  },
  Scope: [AO, [[Scope]]]
}

⑦ 查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出

javascript
ECStack = [
  globalContext
];

2. 闭包

在 JavaScript 中,闭包是一种特性,它允许一个函数访问并操作函数外部的变量。闭包是由函数和与其相关的引用环境组合而成的。

当一个函数嵌套在另一个函数中时,内部函数就可以访问外部函数的变量,即使外部函数已经执行完毕,这些变量也仍然可以被内部函数访问。这就是闭包。

闭包核心:函数作用域链的存在,使得函数在执行完成后,因为被另一个函数的作用域链引用,因此 AO 不会被销毁,但函数执行上下文会弹栈。这样函数也可以顺着作用域链来访问到 AO,即使函数被销毁。

[1] 闭包的访问过程
javascript
function createAdder(count) {
  function adder(num) {
	return count + num
  }

  return adder
}

var adder5 = createAdder(5)
adder5(100)
adder5(55)
adder5(12)

var adder8 = createAdder(8)
adder8(22)
adder8(35)
adder8(7)

console.log(adder5(24))
console.log(adder8(30))

var adder5 = createAdder(5) 执行完成后的内存图。

adder5(100) 执行完成后的内存图。

[2] 闭包缺点

虽然闭包在 JavaScript 中是一个非常强大和有用的特性,但是它也有一些潜在的缺点:

① 内存消耗:闭包可以使得函数中的局部变量在函数执行完毕后仍然被保留在内存中,这可能会导致更多的内存被使用。如果大量使用闭包,或者闭包中保留了大量数据,可能会导致内存消耗过大。

② 性能考虑:创建闭包需要时间和资源,如果在一个性能关键的环境中大量使用闭包,可能会对性能产生影响。

③ 代码可读性:过度使用闭包可能会使得代码难以理解和维护,特别是对于不熟悉闭包概念的开发者来说。

④ 变量污染。

浏览器优化:一个函数内部的某些变量,在这个函数返回的函数中没有使用这些变量,那么 js 引擎是会优化的(不保存这些变量)。

3. 面试题

AO(Activation Object)流程: ① 形参赋值。 ② 寻找函数声明。 ③ 变量声明。 ④ 执行函数。

习题一

javascript
function test(){
  return a;
  a = 1;
  function a(){}
  var a = 2;
}

console.log(test()); // f a(){}

习题二

javascript
console.log(test()); // 2

function test(){
  a = 1;
  function a(){}
  var a = 2;
  return a;
}

习题三

javascript
a = 1;
function test(e){
  function e(){}
  arguments[0] = 2;
  console.log(e);
  if(a){
    var = 3;
  }
  var C;
  a = 4;
  var a;
  console.log(b);
  f = 5:
  console.log(c);
  console.log(a);
}
var a;
test(1);
console.log(a);
console.log(f);

结果如下。

2
undefined
undefined
4
1
5
2)作用域提升面试题

面试题一

javascript
var n = 100
function foo() {
  n = 200
}
foo()
console.log(n)
点我查看答案
    200
  

面试题二

javascript
function foo() {
  console.log(n)
  var n = 200
  console.log(n)
}
var n = 100
foo()
点我查看答案
    undefined
    200
  

面试题三

javascript
var n = 100
function foo1() {
  console.log(n)
}
function foo2() {
  var n = 200
  console.log(n)
  foo1()
}
foo2()
console.log(n)
点我查看答案
    200
    100
    100
  

面试题四

javascript
var a = 100
function foo() {
  console.log(a)
  return
  var a = 100
}
foo()
点我查看答案
    undefined
  
**面试题五**
javascript
function foo() {
  var a = b = 100
}
foo()
console.log(a)
console.log(b)
点我查看答案
    ReferenceError: a is not defined
    100
  

第三章:常见内置类

一、原始类型的包装类

JavaScript 的原始类型并非对象类型,所以从理论上来说,它们是没有办法获取属性或者调用方法的。但是,在开发中会看到这样操作:

js
// 原始字符串
let strPrimitive = "Hello, World!";
console.log(typeof strPrimitive); // 输出 "string"

// 使用原始字符串的length属性
console.log(strPrimitive.length); // 输出 13

// 使用原始字符串的toUpperCase方法
console.log(strPrimitive.toUpperCase()); // 输出 "HELLO, WORLD!"

// String包装对象
let strObject = new String("Hello, World!");
console.log(typeof strObject); // 输出 "object"

// 使用String对象的length属性
console.log(strObject.length); // 输出 13

// 使用String对象的toUpperCase方法
console.log(strObject.toUpperCase()); // 输出 "HELLO, WORLD!"

那么,为什么会出现这样奇怪的现象呢?(悖论)

因为 JavaScript 为了使其可以获取属性和调用方法对其封装了对应的包装类型。当调用一个原始类型的属性或者方法时,会进行如下操作:

① 根据原始值创建一个原始类型对应的包装类型对象。

② 调用对应的属性或者方法,返回一个新的值。

③ 创建的包装类对象被销毁。

通常 JavaScript 引擎会进行很多的优化,它可以跳过创建包装类的过程在内部直接完成属性的获取或者方法的调用。

常见的包装类型有:String、Number、Boolean、Symbol、BigInt 类型。

1. Number 类

属性

Number.MAX_SAFE_INTEGER:JavaScript 中最大的安全整数 (2^53 - 1) 。

Number.MIN_SAFE_INTEGER:JavaScript 中最小的安全整数 --(2^53 - 1) 。

Number 实例方法

方法一:toString(base),将数字转成字符串,并且按照 base 进制进行转化。

      base 的范围可以从 2 到 36,默认情况下是 10。

注意:如果是直接对一个数字操作,需要使用运算符。

123.toString(2); // 语法错误 因为会把 . 看成小数点。(123).toString(2); // "1111011"

方法二:toFixed(digits),格式化一个数字,保留 digits 位的小数。

digits 的范围是 0 到 20(包含)之间。

Number 类方法

方法一:Number.parseInt(string[, radix]),将字符串解析成整数,也有对应的全局方法 parseInt。

方法二:Number.parseFloat(string),将字符串解析成浮点数,也有对应的全局方法 parseFloat。

2. String 类

见数据类型章节。

二、Math 对象

Math 是一个内置对象(不是一个构造函数),它拥有一些数学常数属性和数学函数方法。

Math 常见的属性

Math.PI:圆周率,约等于 3.14159。

Math 常见的方法

Math.floor():向下舍入取整。

Math.ceil():向上舍入取整。

Math.round():四舍五入取整。

Math.random():生成 0~1 的随机数(包含 0,不包含 1)。

Math.pow(x, y):返回 x 的 y 次幂。

数字Math.floorMath.ceilMath.round
3.1343
3.6344
3.644344
3.46343

生成一个在 min 和 max 之间(包括 min 和 max)的随机整数。

javascript
Math.floor(Math.random() * (max - min + 1)) + min;

三、Date 对象

时间的计算方式有两种:GMT 与 UTC。

时间的表示方式有两种:RFC 2822 标准或者 ISO 8601 标准。一个典型的 RFC 2822 日期时间字符串可能看起来像这样:"Tue, 26 Jan 2024 19:33:40 +0000"。ISO 8601 是国际标准的日期和时间表示方法,其格式为:"YYYY-MM-DDTHH:mm:ss.sssZ(+/-)HH:MM"。

1. 创建 Date 对象

创建当前日期和时间的 Date 对象:

javascript
let currentDate = new Date();
console.log(currentDate); // Mon Mar 17 2024 18:46:07 GMT+0800 (中国标准时间)

表示自 1970年1月1日 00:00:00 UTC(Unix Epoch,即 Unix 时间起点)以来的毫秒数。

javascript
let date = new Date(1000);
console.log(date.toUTCString()); // 输出 "Thu, 01 Jan 1970 00:00:01 GMT

创建特定日期的 Date 对象(格式为:年-月-日):

javascript
let specificDate = new Date("2024-01-26");
console.log(specificDate); // Fri Jan 26 2024 08:00:00 GMT+0800 (中国标准时间)

创建特定日期和时间的 Date 对象(格式为:年-月-日T小时:分钟:秒):

javascript
let specificDateTime = new Date("2024-01-26T19:33:40");
console.log(specificDateTime); // Fri Jan 26 2024 19:33:40 GMT+0800 (中国标准时间)

创建特定日期和时间的 Date 对象,使用年,月,日,小时,分钟,秒和毫秒作为参数(注意:月份是从 0 开始的,所以 1 表示 2 月):

javascript
let specificDateTime2 = new Date(2024, 0, 26, 19, 33, 40);
console.log(specificDateTime2);

2. Date 获取信息的方法

getFullYear():获取年份 4 位数。

getMonth():获取月份,从 0 到 11。

getDate():获取当月的具体日期,从 1 到 31(方法名字有点迷)。

getHours():获取小时。

getMinutes():获取分钟。

getSeconds():获取秒钟。

getMilliseconds():获取毫秒。

getDay():获取一周中的第几天,从 0(星期日)到 6(星期六)。

3. Date 获取 Unix 时间戳

Unix 时间戳 :它是一个整数值,表示自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数。

在 JavaScript 中,我们有多种方法可以获取这个时间戳:

方式一:new Date().getTime()

方式二:new Date().valueOf()

方式三:+new Date()

方式四:Date.now()

4. Date.parse 方法

Date.parse(str) 方法可以从一个字符串中读取日期 ,并且输出对应的 Unix 时间戳。

Date.parse(str) 作用等同于 new Date(dateString).getTime() 操作。

需要符合 RFC2822 或 ISO8601 日期格式的字符串。比如 YYYY-MM-DDTHH:mm:ss.sssZ

其他格式也许也支持,但结果不能保证一定正常。如果输入的格式不能被解析,那么会返回 NaN。

javascript
// 解析ISO 8601格式
let timestamp1 = Date.parse("2024-03-17T14:30:00Z");
console.log(timestamp1); // 1710685800000

// 解析简单日期字符串
let timestamp2 = Date.parse("March 17, 2024");
console.log(timestamp2); // 返回值因浏览器和本地时区而异

第四章:面向对象

理解 JavaScript 中的对象。如何创建对象,对象的属性和方法,以及原型和原型链的概念。

一、对象

1. 为什么要有面向对象编程?

对象是包括属性与方法的数据类型,JS 中大部分类型都是对象。如 String/Number/Math/RegExp/Date 等。

面向过程编程

javascript
let name = "张三";
let grade = [
  { lesson: "js", score: 99 },
  { lesson: "mysql", score: 85 }
];

function average(grade, name) {
  const total = grade.reduce((t, a) => t + a.score, 0);
  return name + ":" + total / grade.length + "分";
}

console.log(average(grade, name));

面向对象编程

下面使用对象编程的代码结构清晰,也减少了函数的参数传递,也不用担心函数名的覆盖。

javascript
let user = {
  name: "张三",
  grade: [
    { lesson: "js", score: 99 },
    { lesson: "mysql", score: 85 }
  ],
  average() {
    const total = this.grade.reduce((t, a) => t + a.score, 0);
    return this.name + ":" + total / this.grade.length + "分";
  }
};
console.log(user.average());

2. 创建对象

对象字面量语法:使用花括号 {} 创建对象,并直接在括号内定义属性和方法。例如:

javascript
let person = {
  name: "Alice",
  age: 25,
  greet: function() {
    console.log("Hello, my name is " + this.name);
  }
};

方法的简写。

javascript
let person = {
  name: "Alice",
  age: 25,
  greet() {
    console.log("Hello, my name is " + this.name);
  }
};

其实字面量形式在系统内部也是使用构造函数 new Object 创建的。

javascript
let aaa = {};
let obj = new Object();
console.log(aaa, obj); // {} {}
console.log(aaa.constructor); // [Function: Object]
console.log(obj.constructor); // [Function: Object]

构造函数方法:通过定义一个构造函数,然后使用 new 关键字创建对象实例。

javascript
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.greet = function() {
    console.log("Hello, my name is " + this.name);
  };
}

let person = new Person("Alice", 25);

Object.create() 方法:它创建一个新对象,并将这个新对象的原型设置为指定的对象。

javascript
var obj = { a: 1 };
var newObj = Object.create(obj);

console.log(newObj.a); // 输出:1

在这个例子中,首先创建了一个对象 obj,然后使用 Object.create(obj) 创建了一个新对象 newObj。newObj 的原型被设置为 obj,所以 newObj 可以访问 obj 的属性 a。

Object.create() 还可以接受一个可选的第二个参数,这个参数是一个属性描述符对象,用于定义新对象的额外属性。例如:

javascript
var obj = { a: 1 };
var newObj = Object.create(obj, {
  b: { value: 2, writable: true, enumerable: true, configurable: true }
});

console.log(newObj.b); // 输出:2

这个例子中,在 newObj 上定义了一个新属性 b,并设置了它的值和属性描述符。

对象的属性最终都会转为字符串。

javascript
let obj = { 1: "张三", "1": "李四" };
console.table(obj); // {1:"李四"}

使用对象做为键名时,会将对象转为字符串后使用。

javascript
let obj = { 1: "张三", "1": "李四" };

let bar = { [obj]: "王五" };
console.table(bar);

// 如何取出王五,以下两种二选一
console.log(bar["[object Object]"]);
console.log(bar[obj.toString()]);

3. 属性管理

获取属性

使用点语法获取。

javascript
let user = {
  name: "张三"
};
console.log(user.name);

使用 [] 获取。

javascript
console.log(user["name"]);

可以看出使用 . 操作属性更简洁。[] 主要用于通过变量定义属性的场景。

javascript
let user = {
  name: "张三"
};
let property = "name";
console.log(user[property]);

如果属性名不是合法变量名就必须使用扩号的形式了。

javascript
let user = {};
user["my name"] = "张三";
user["my-age"] = 28;
console.log(user["my name"]);
console.log(user["my-age"]);

添加属性

可以为对象添加属性。

javascript
let obj = {name: "张三"};
obj.site = "baidu.com";
console.log(obj);

删除属性

使用 delete 可以删除属性。

javascript
let obj = { name: "张三" };
delete obj.name;
console.log(obj.name); // undefined

检测属性

hasOwnProperty 检测对象自身是否包含指定的属性,不检测原型链上继承的属性。

javascript
let obj = { name: '张三'};
console.log(obj.hasOwnProperty('name')); // true

而使用 in 可以在原型对象上检测。下面通过数组查看。

javascript
let arr = ["张三"];

console.log(arr.hasOwnProperty("length")); // true
console.log(arr.hasOwnProperty("concat")); // false
console.log("concat" in arr); // true
javascript
let obj = {name: "张三"};
let aaa = {
  web: "baidu.com"
};

// 设置 aaa 为 obj 的新原型
Object.setPrototypeOf(obj, aaa);
console.log(obj);

console.log(obj.hasOwnProperty("web")); // false
console.log("web" in obj); // true

合并属性 assign

从一个或多个对象复制属性。

javascript
"use strict";
let bar = { a: 1, b: 2 };
bar = Object.assign(bar, { f: 1 }, { m: 9 });
console.log(bar); // {a: 1, b: 2, f: 1, m: 9}

计算属性

对象属性可以通过表达式计算定义,这在动态设置属性或执行属性方法时很好用。

javascript
let id = 0;
const user = {
  [`id-${id++}`]: id,
  [`id-${id++}`]: id,
  [`id-${id++}`]: id
};
console.log(user);

使用计算属性为文章定义键名。

javascript
const lessons = [
  {
    title: "媒体查询响应式布局",
    category: "css"
  },
  {
    title: "FLEX 弹性盒模型",
    category: "css"
  },
  {
    title: "MYSQL多表查询随意操作",
    category: "mysql"
  }
];

let lessonObj = lessons.reduce((obj, cur, index) => {
  obj[`${cur["category"]}-${index}`] = cur;
  return obj;
}, {});

console.log(lessonObj); // {css-0: {…}, css-1: {…}, mysql-2: {…}}
console.log(lessonObj["css-0"]); // {title: "媒体查询响应式布局", category: "css"}

4. 属性描述符

[1] Object.defineProperty / Object.defineProperties

Object.defineProperty() 方法接受三个参数:

① 目标对象:这是你想要添加或修改属性的对象。

② 属性名:这是你想要添加或修改的属性的名称。

③ 属性描述符:这是一个对象,包含了你想要给这个属性设置的特性。

返回值:被传递给函数的对象。

Object.defineProperties 方法可以在一个对象上定义新的属性或修改现有属性,然后返回该对象。这个方法接受两个参数:第一个参数是要被添加属性或修改属性的对象,第二个参数是一个对象,其中每个键都对应一个要被添加到第一个参数对象上的属性描述符。

javascript
let obj = {};

Object.defineProperties(obj, {
  'property1': {
    value: true,
    writable: true
  },
  'property2': {
    value: 'Hello',
    writable: false
  },
  'property3': {
    get: function() { return this.property2; },
    set: function(value) { this.property2 = value; }
  }
});

console.log(obj.property1); // 输出:true
console.log(obj.property2); // 输出:"Hello"
console.log(obj.property3); // 输出:"Hello"

obj.property3 = 'World';
console.log(obj.property2); // 输出:"World"
console.log(obj.property3); // 输出:"World"

属性描述符分类

属性描述符的类型有两种:

  • 数据属性(Data Properties)描述符(Descriptor)。
  • 存取属性(Accessor 访问器 Properties )描述符(Descriptor)。
configurableenumerablevaluewritablegetset
数据描述符可以可以可以可以不可以不可以
存取描述符可以可以不可以不可以可以可以
[2] 数据属性描述符

数据描述符有如下四个特性:

  • [[Configurable]]:表示属性是否可以通过 delete 删除属性;是否可以修改它的特性;或者是否可以将它修改为存取属性描述符;

    当我们直接在一个对象上定义某个属性时,这个属性的 [[Configurable]] 为 true。

    当我们通过属性描述符定义一个属性时,这个属性的 [[Configurable]] 为 false。

  • [[Enumerable]]:表示属性是否可以通过 for-in 或者 Object.keys() 返回该属性。

    当我们直接在一个对象上定义某个属性时,这个属性的 [[Enumerable]] 为 true。

    当我们通过属性描述符定义一个属性时,这个属性的 [[Enumerable]] 为 false。

  • [[Writable]]:表示是否可以修改属性的值。

    当我们直接在一个对象上定义某个属性时,这个属性的 [[Writable]] 为 true。

    当我们通过属性描述符定义一个属性时,这个属性的 [[Writable]] 为 false。

  • [[value]]:属性的 value 值,读取属性时会返回该值,修改属性时,会对其进行修改。

    默认情况下这个值是 undefined。

[3] 存取属性描述符

存取描述符有如下四个特性:

  • [[Configurable]]:表示属性是否可以通过 delete 删除属性;是否可以修改它的特性;或者是否可以将它修改为存取属性描述符。

    和数据属性描述符是一致的。

    当我们直接在一个对象上定义某个属性时,这个属性的 [[Configurable]] 为 true。

    当我们通过属性描述符定义一个属性时,这个属性的 [[Configurable]] 为 false。

  • [[Enumerable]]:表示属性是否可以通过 for-in 或者 Object.keys() 返回该属性。

    和数据属性描述符是一致的。

    当我们直接在一个对象上定义某个属性时,这个属性的 [[Enumerable]] 为 true。

    当我们通过属性描述符定义一个属性时,这个属性的 [[Enumerable]] 为 false。

  • [[get]]:获取属性时会执行的函数。默认为 undefined。

  • [[set]]:设置属性时会执行的函数。默认为 undefined。

[4] Object 静态方法补充
  • 获取对象的属性描述符:getOwnPropertyDescriptor(对象, 属性)、getOwnPropertyDescriptors(对象)

  • 禁止对象扩展新属性:preventExtensions

    给一个对象添加新的属性会失败(在严格模式下会报错)。

  • 密封对象,不允许配置和删除属性:seal

    实际是调用 preventExtensions,并且将现有属性设置为 configurable:false。值得注意的是,密封的对象的属性值仍然可以被改变。

  • 冻结对象,不允许修改现有属性:freeze

    Object.freeze 方法实际上是在调用 Object.seal 方法的基础上,将对象的所有现有属性的 writable 属性设置为 false,这样就不能修改这些属性的值了。

操作preventExtensionssealfreeze
添加属性
删除属性
修改属性值
修改描述符

5. ES6 新特性

1)展开语法

数组那里说过了。

2)解构赋值

基本使用

下面是基本使用语法。

javascript
// 对象使用
let info = {name:'张三', url:'xxx.com'};
let {name:n, url:u} = info
console.log(n); // 张三

// 如果属性名与变量相同可以省略属性定义
let {name,url} = {name:'张三', url:'xxx.com'};
console.log(name); // 张三

函数返回值直接解构到变量。

javascript
function foo() {
  return {
    name: '张三',
    url: 'xxx.com'
  };
}
let {name: n,url: u} = foo();
console.log(n);

函数传参。

javascript
"use strict";
function foo({ name, age }) {
  console.log(name, age); // 张三 18
}
foo({ name: "张三", age: 18 });

系统函数解构练习,这没有什么意义,只是加深解构印象。

javascript
const {random} = Math;
console.log(random());

严格模式

非严格模式可以不使用声明指令,严格模式下必须使用声明。所以建议使用 let 等声明。

javascript
({name,url} = {name:'张三', url:'xxx.com'});
console.log(name, url);

还是建议使用 let 等赋值声明。

javascript
"use strict";
let { name, url } = {name:'张三', url:'xxx.com'};
console.log(name, url);

简洁定义

只赋值部分变量。

javascript
let [,url]=['张三','xxx.com'];
console.log(url); // xxx.com

let {name}= {name:'张三', url:'xxx.com'};
console.log(name); // 张三

可以直接使用变量赋值对象属性。

javascript
let name = "张三",url = "xxx.com";
// 标准写法如下
let bar = { name: name, url: url };
console.log(bar);  // {name: "张三", url: "xxx.com"}

// 如果属性和值变量同名可以写成以下简写形式
let opt = { name, url };
console.log(opt); // {name: "张三", url: "xxx.com"}

嵌套解构

可以操作多层复杂数据结构。

javascript
const bar = {
  name:'张三',
  lessons:{
    title:'JS'
  },
  friends: ['张三', '李四', '王五']
}

const {name,lessons:{title},friends:[name1,,name3]}  = bar;
console.log(name,title); // 张三 JS

默认值

为变量设置默认值。

javascript
let [name, site = 'xxx'] = ['张三'];
console.log(site); // xxx

let {name,url,user='张三'}= {name:'李四',url:'xxx.com'};
console.log(name,user); // 李四 张三

使用默认值特性可以方便的对参数预设。

javascript
function createElement(options) {
  let {
    width = '200px',
    height = '100px',
    backgroundColor = 'red'
  } = options;

  const h2 = document.createElement('h2');
  h2.style.width = width;
  h2.style.height = height;
  h2.style.backgroundColor = backgroundColor;
  document.body.appendChild(h2);
}

createElement({
  backgroundColor: 'green'
});

函数参数

数组参数的使用。

javascript
function foo([a, b]) {
  console.log(a, b);
}

foo(['张三', '李四']);

对象解构传参。

javascript
function user(name, { sex, age } = {}) {
  console.log(name, sex, age); // 张三 男 18
}

user("张三", { sex: "男", age: 18 });

5. 遍历对象

1)获取内容

使用系统提供的 API 可以方便获取对象属性与值。

javascript
const bar = {
  name: "张三",
  age: 10
};
console.log(Object.keys(bar)); // ["name", "age"]
console.log(Object.values(bar)); // ["张三", 10]
console.table(Object.entries(bar)); // [["name","张三"],["age",10]]
2)for / in

使用 for/in 遍历对象属性。

javascript
const bar = {
  name: "张三",
  age: 10
};
for (let key in bar) {
  console.log(key, bar[key]);
}
3)for / of

for/of 用于遍历迭代对象,不能直接操作对象。但 Object 对象的 keys() 方法返回的是迭代对象。

javascript
const bar = {
  name: "张三",
  age: 10
};

for (const key of Object.keys(bar)) {
  console.log(key);
}

获取所有对象属性。

javascript
const bar = {
  name: "张三",
  age: 10
};
for (const v of Object.values(bar)) {
  console.log(v);
}

同时获取属性名与值

javascript
for (const array of Object.entries(bar)) {
  console.log(array);
}

使用扩展语法同时获取属性名与值

javascript
for (const [key, value] of Object.entries(bar)) {
  console.log(key, value);
}

举个例子:添加元素 DOM 练习。

javascript
let lessons = [
  { name: "js", click: 23 },
  { name: "node", click: 192 }
];

let ul = document.createElement("ul");

for (const val of lessons) {
  let li = document.createElement("li");
  li.innerHTML = `课程:${val.name},点击数:${val.click}`;
  ul.appendChild(li);
}

document.body.appendChild(ul);

6. 对象拷贝

对象赋值时复制的是内存地址,所以一个对象的改变直接影响另一个。

javascript
let obj = {
  name: '张三',
  user: {
  	name: '田七'
  }
}
let a = obj;
let b = obj;
a.name = 'lisi';
console.log(b.name); // lisi
1)浅拷贝

使用 for/in 执行对象拷贝。

javascript
let obj = {name: "张三"};

let bar = {};
for (const key in obj) {
  bar[key] = obj[key];
}

bar.name = "李四";
console.log(bar); // { name: '李四' }
console.log(obj); // { name: '张三' }

除了上面的方法,还可以使用 Object.assign() 函数来简单的实现浅拷贝,它是将前一个对象的属性叠加后面对象属性,会覆盖前面对象同名属性。

javascript
let user = {
  name: '张三'
};

let bar = {
  stu: Object.assign({}, user)
};

bar.stu.name = '李四';
console.log(user.name); // 张三

使用展示语法也可以实现浅拷贝。

javascript
let obj = {
  name: "张三"
};

let bar = { ...obj };
bar.name = "李四";

console.log(bar);
console.log(obj);
2)深拷贝

浅拷贝不会将深层的数据复制。

javascript
let obj = {
    name: '张三',
    user: {
        name: '李四'
    }
}
let a = obj;
let b = obj;

function copy(object) {
    let obj = {}
    for (const key in object) {
        obj[key] = object[key];
    }
    return obj;
}
let newObj = copy(obj);

newObj.name = '赵六';
newObj.user.name = '田七';
console.log(newObj);
console.log(obj);

深拷贝是完全的复制一个对象,两个对象是完全独立的对象。

javascript
let obj = {
  name: "张三",
  user: {
    name: "李四"
  },
  data: []
};

function copy(object) {
  let obj = object instanceof Array ? [] : {};
  for (const [k, v] of Object.entries(object)) {
    obj[k] = typeof v == "object" ? copy(v) : v;
  }
  return obj;
}

let bar = copy(obj);

bar.data.push("赵六");
console.log(JSON.stringify(bar, null, 2));
console.log(JSON.stringify(obj, null, 2));

上面代码的输出结果如下。

{
  "name": "张三",
  "user": {
    "name": "李四"
  },
  "data": [
    "赵六"
  ]
}
{
  "name": "张三",
  "user": {
    "name": "李四"
  },
  "data": []
}

8. 构造函数

对象可以通过内置或自定义的构造函数创建。

1)分类
[1] 工厂函数

在函数中返回对象的函数称为工厂函数,工厂函数有以下优点。

  • 减少重复创建相同类型对象的代码。
  • 修改工厂函数的方法影响所有同类对象。

使用字面量创建对象需要复制属性与方法结构。

javascript
const uer1 = {
  name: "张三",
  show() {
    console.log(this.name);
  }
};

const uer2 = {
  name: "李四",
  show() {
    console.log(this.name);
  }
};

使用工厂函数可以简化这个过程。

javascript
function stu(name) {
  return {
    name,
    show() {
      console.log(this.name);
    }
  };
}

const lisi = stu("李四");
lisi.show();
const lz = stu("王五");
lz.show();
[2] 构造函数

和工厂函数相似,构造函数也用于创建对象,它的上下文为新的对象实例。

  • 构造函数名每个单词首字母大写即 Pascal 命名规范(大驼峰)。
  • this 指当前创建的对象。
  • 不需要返回 this 系统会自动完成。
  • 需要使用 new 关键词生成对象。
javascript
function Student(name) {
  this.name = name;
  this.show = function() {
    console.log(this.name);
  };
  // 不需要返回,系统会自动返回
  // return this;
}

const lisi = new Student("李四");
lisi.show();
const lz = new Student("王五");
lz.show();

如果构造函数返回对象,实例化后的对象将是此对象。

javascript
function ArrayObject(...values) {
  const arr = new Array();
  arr.push.apply(arr, values);
  arr.string = function(sym = "|") {
    return this.join(sym);
  };
  return arr;
}

const array = new ArrayObject(1, 2, 3);
console.log(array);
console.log(array.string("-"));

需要注意,在严格模式下方法中的 this 值为 undefined,这是为了防止无意的修改 window 对象。

javascript
"use strict";
function User() {
  this.show = function() {
    console.log(this);
  };
}

let user = new User();
user.show(); // User

// 之后加上严格模式关键字
let lz = user.show;
lz(); // undefined
[3] 内置构造

JS 中大部分数据类型都是通过构造函数创建的。

javascript
const num = new Number(99);
console.log(num.valueOf());

const string = new String("张三");
console.log(string.valueOf());

const boolean = new Boolean(true);
console.log(boolean.valueOf());
console.log(boolean.toString());

const date = new Date();
console.log(date.valueOf()); // date.valueOf() 方法返回的是 date 对象代表的时间戳(以毫秒为单位)

const regexp = new RegExp("\\d+");
console.log(regexp.test(99));

let lz = new Object();
lz.name = "张三";
console.log(lz);

字面量创建的对象,内部也是调用了 Object 构造函数。

javascript
const bar = {
  name: "张三"
};
console.log(bar.constructor); // ƒ Object() { [native code] }

// 下面是使用构造函数创建对象
const cms = new Object();
cms.title = "开源内容管理系统";
console.log(cms);
[4] 对象函数

在 JS 中,函数也是一个对象。

javascript
function foo(name) {}

console.log(foo.toString());
console.log(foo.length); // 输出的是函数 foo 的参数数量

函数是由系统内置的 Function 构造函数创建的。

javascript
function foo(name) {}

console.log(foo.constructor); // Function

下面是使用内置构造函数创建的函数。

javascript
const User = new Function(`name`,`
  this.name = name;
  this.show = function() {
    return this.name;
  };
`
);

const lisi = new User("李四");
console.log(lisi.show());

/* 上面使用 function 函数创建对象,也可以改写为如下 */
function foo(name) {}
const User = foo.constructor(`name`,`
  this.name = name;
  this.show = function() {
    return this.name;
  };
`
);

9. 代理拦截

1)基础知识
[1] Reflect

Reflect 是 JavaScript 中的一个内置对象,它提供了一系列静态方法来执行与对象相关的操作。Reflect 对象的方法与 Proxy 对象的方法相对应,通常用于在 Proxy 拦截器中进行操作。以下是 Reflect 对象中一些常用的方法:

Reflect.get(target, property[, receiver]) : 获取目标对象的属性值。

Reflect.set(target, property, value[, receiver]) : 设置目标对象的属性值。

Reflect.has(target, property) : 检查目标对象是否有某个属性。

Reflect.deleteProperty(target, property) : 删除目标对象的属性。

Reflect.construct(target, args[, newTarget]) : 作为构造函数调用目标函数。

这些方法与直接在对象上执行的操作相对应,但是它们提供了更明确和一致的语义,尤其是在处理 Proxy 对象时。

下面是使用 Reflect 的一些示例:

javascript
const obj = {
  name: 'Alice',
  age: 25
};

// 使用 Reflect.get() 获取属性值
console.log(Reflect.get(obj, 'name')); // 输出: Alice

// 使用 Reflect.set() 设置属性值
Reflect.set(obj, 'age', 30);
console.log(obj.age); // 输出: 30

// 使用 Reflect.has() 检查属性是否存在
console.log(Reflect.has(obj, 'name')); // 输出: true
console.log(Reflect.has(obj, 'address')); // 输出: false

// 使用 Reflect.deleteProperty() 删除属性
Reflect.deleteProperty(obj, 'age');
console.log(obj.age); // 输出: undefined

通过使用 Reflect 对象的方法,我们可以更清晰地表达我们的意图,并在处理 Proxy 对象时获得更好的控制。

[2] Proxy

在 JavaScript 中,代理通常是通过使用 Proxy 对象来实现的。Proxy 对象用于创建一个对象的代理,从而可以拦截和修改基本操作的行为。通过 Proxy,你可以定义在访问对象属性或调用对象方法时要执行的自定义行为。

下面是一个简单的示例,展示了如何在 JavaScript 中使用 Proxy 实现代理:

javascript
// 创建一个原始对象
const originalObject = {
  name: 'Alice',
  age: 25
};

// 创建一个代理对象
const proxyObject = new Proxy(originalObject, {
  get(target, property) {
    console.log(`Reading property: ${property}`);
    return Reflect.get(target, property); // 使用 Reflect.get() 获取属性值
  },
  set(target, property, value) {
    console.log(`Setting property: ${property}`);
    return Reflect.set(target, property, value); // 使用 Reflect.set() 设置属性值
  }
});

// 通过代理对象访问属性
console.log(proxyObject.name); // 输出: Reading property: name Alice

// 通过代理对象设置属性
proxyObject.age = 30; // 输出: Setting property: age

在上面的示例中,我们创建了一个原始对象 originalObject 和一个代理对象 proxyObject。代理对象是通过使用 Proxy 构造函数创建的,它接受原始对象和一个处理程序对象作为参数。处理程序对象定义了在访问属性或设置属性时要执行的行为。在这个例子中,我们使用 console.log() 记录操作,并使用 Reflect.get() 和 Reflect.set() 执行实际的属性访问和设置操作。通过这种方式,我们可以拦截和监视对原始对象的访问和修改操作。

陷阱(trap)函数是什么?

陷阱(trap)函数在 JavaScript 中是通过 Proxy 对象使用的。Proxy 对象允许你创建一个代理,该代理可以拦截和修改对象的基本操作。你可以在创建 Proxy 对象时提供一个 handler 对象,这个 handler 对象的属性就是陷阱函数。

陷阱函数的使用分为以下几个步骤:

① 定义目标对象(Target Object): 首先,你需要定义一个目标对象,这个对象是你想要拦截其操作的原始对象。

javascript
let target = {
  name: 'Alice',
  age: 25
};

② 定义陷阱函数(Trap Functions): 定义一个 handler 对象,它包含一组陷阱函数。每个陷阱函数对应一个你想要拦截的基本操作。

陷阱函数的参数规则如下:

  • target:陷阱函数接收的第一个参数始终是目标对象。这是在陷阱函数中访问和操作原始对象的引用。
  • prop:对于属性访问陷阱(如 get 和 set),第二个参数是正在访问或修改的属性名。它是一个字符串或 Symbol,表示正在访问的属性。
  • receiver:接收者对象。它通常是代理对象自身,但在某些情况下,可能是代理对象的原型。这个参数用于确保正确的 this 上下文。

对于 Reflect 方法的调用,陷阱函数通常会将接收到的参数直接传递给相应的 Reflect 方法。这是因为 Reflect 方法与陷阱函数的参数是一致的,这样做可以简化代码并确保正确的行为。

需要注意的是,陷阱函数的参数规则可能会因具体的陷阱类型而有所不同。某些陷阱函数可能会接收额外的参数或具有不同的参数顺序。因此,在定义陷阱函数时,最好参考 JavaScript 文档以了解特定陷阱函数的参数规则。

javascript
let handler = {
  get(target, prop, receiver) {
    console.log(`Reading ${prop}`);
    return Reflect.get(...arguments);
  },
  set(target, prop, value, receiver) {
    console.log(`Writing ${prop} with ${value}`);
    return Reflect.set(...arguments);
  }
};

在这个例子中,我们定义了两个陷阱函数:get 和 set。get 陷阱函数在读取属性值时会被调用,而 set 陷阱函数在设置属性值时会被调用。

③ 创建 Proxy 对象: 使用 Proxy 构造函数,将目标对象和陷阱函数传递给它,以创建一个 Proxy 对象。

javascript
let proxy = new Proxy(target, handler);

④ 使用 Proxy 对象: 最后,你可以像使用普通对象一样使用 Proxy 对象。当对 Proxy 对象执行操作时,相应的陷阱函数将被调用。

javascript
console.log(proxy.name);  // 输出: Reading name 和 Alice
proxy.name = 'Bob';       // 输出: Writing name with Bob

在这个例子中,当我们尝试读取 proxy.name 的值时,get 陷阱函数被调用。同样,当我们尝试设置 proxy.name 的值时,set 陷阱函数被调用。

通过使用陷阱函数,你可以自定义对象的行为,实现更高级的应用,如数据验证、访问控制、虚拟化等。

常见的陷阱函数

陷阱函数解释
get(target, property, receiver)拦截对象属性的读取操作
set(target, property, value, receiver)拦截对象属性的设置操作
has(target, property)拦截 in 操作符
deleteProperty(target, property)拦截 delete 操作符
apply(target, thisArg, argumentsList)拦截函数调用
触发时机:
① 直接调用:proxy(...args)
② 使用 call:proxy.call(context, ...args)
③ 使用 apply:proxy.apply(context, args)
④ 使用 Reflect.apply:Reflect.apply(proxy, context, args)
construct(target, argumentsList, newTarget)拦截 new 操作符
ownKeys(target)拦截 Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols() 等
defineProperty(target, propertyKey, attributes)拦截 Object.defineProperty() 操作
2)例子

例一:计算函数执行时间

javascript
function factorial(num) {
  return num == 1 ? 1 : num * factorial(num - 1);
}

let proxy = new Proxy(factorial, {
  apply(func, obj, args) {
    console.time("run");
    let res = func.apply(obj, args); // 实际上调用了原始的 factorial 函数,并将参数传递给它
    console.log(res);
    console.timeEnd("run");
  }
});

proxy.call(this, 3);

二、原型

1. 是什么

在 JavaScript 中,原型是一种复杂的类型,它是 JavaScript 实现对象间继承和属性共享的主要机制。每个 JavaScript 对象(null 除外)都具有一个叫做 [[Prototype]] 的内部属性,这个属性是一个指向它的原型对象的链接。

当试图访问一个对象的属性时,如果对象自身没有这个属性,那么 JavaScript 会沿着这个对象的原型链去寻找这个属性。原型链是由对象的 [[Prototype]] 指针构成的链状结构,链的尽头是 null。

每个函数都有一个 prototype 属性,这个属性是一个指向原型对象的指针。当使用 new 运算符创建一个新对象时,新对象的 [[Prototype]] 就会被赋值为构造函数的 prototype 对象。这样,新对象就可以访问原型对象上的属性和方法。

引用《你不知道的 JavaScript》中的话,就是:
继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性。相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。

2. 使用原型的好处?

使用原型(prototype)可以解决在 JavaScript 中通过构造函数创建对象时因为复制多个函数所造成的内存占用问题。

在 JavaScript中,每当我们使用构造函数创建一个新的对象时,如果这个构造函数内部有很多方法,那么每一个新创建的对象都会复制这些方法,这就会造成内存的浪费。因为大部分对象实例通常都会使用相同的方法。

而原型是 JavaScript 中一个对象,它可以使得所有由同一个构造函数创建的对象实例共享相同的方法和属性。这样,所有的对象实例就可以访问和使用原型中的属性和方法,而不需要在每个对象实例中重复创建,从而节省内存。

3. js 原型体现

1)对象的原型

JavaScript 当中每个对象都有一个特殊的内置属性 [[prototype]],这个特殊的对象可以指向另外一个对象。那么这个对象有什么用呢?

当我们通过引用对象的属性 key 来获取一个 value 时,它会触发 [[get]] 的操作;这个操作会首先检查该对象是否有对应的属性,如果有的话就使用它;如果对象中没有改属性,那么会访问对象 [[prototype]] 内置属性指向的对象上的属性。

对象原型也叫隐式原型。

[1] 获取原型对象方式

方式一:通过对象的 _proto_ 属性可以获取到(这是早期浏览器自己添加的,存在一定的兼容性问题);

方式二:通过 Object.getPrototypeOf 方法可以获取到;

本质:devtools 里的 [[prototype]] 其实就是 _proto_ 对应的 getter 函数的返回值。

[2] __proto__ 解析

本质

_proto_ 本质是 Object.prototype 中的 getter / setter 属性,它具有 magic 效应。

javascript
let xy = {a:1};
console.dir(xy);

在浏览器控制台可以看到 xy 对象的原型是 Object.prototype,按理说它的 _proto_ 为 null,但我们找不到。这是因为浏览器对于 [[Prototype]]: null 时不显示。

我们还注意到了 Object.prototype 原型对象上有 _proto_?这个是 getter/setter 属性。

关于浏览器不显示 [[Prototype]]: null:[将 [prototype]] 内部属性也添加到 Object.prototype 中

调用 getter 的时候 this 是什么?

javascript
proto = {
  get greet() {
    return this.city
  }
}

a = { __proto__: proto, city: 'a' }
b = { __proto__: proto, city: 'b' }

比如我们展开的是 b:

有两个 greet 属性,一个是 b 从原型上继承的,一个是通过 [[prototype]] 展开的原型的自身属性,两者求值后都是 'b'。

为什么?因为原型链上的 getter/setter 和方法类似,都是为继承它的对象准备的,而不是为自己。所以 greet 函数执行时的 this 都是指向调用者。

为什么 Object.prototype 中的 __proto__ 不为 NULL?

javascript
let xy = {a:1};
console.dir(xy);

第一次的 __proto__ 是求值时,相当于执行的是 xy.__proto__ 而不是 Object.prototype.__proto__;第二次在求值时因为当前对象已经成了 Object.prototype 所以才返回 null 了。

问:为什么 greet 属性会直接出现在-继承的对象里,而 _proto_ 不会?

答:特殊处理故意隐藏了。因为有了 [[prototype]],没必要展现 _proto_。

随堂练习

javascript
const str = new String('123')

// 控制台查看下面的代码,会输出什么
str // String {'123'}
str.__proto__ === String.prototype // true
String.prototype.__proto__ === Object.prototype // true 
Object.prototype.__proto__ === null // true
// 其实就是:
str.__proto__.__proto__.__proto__ === null // true

但是顺着控制台原型链查找,要找 5 次才到 null ???

javascript
str.[[Prototype]].[[Prototype]].__proto__.[[Prototype]].__proto__ === null
点我打开浏览器控制台显示的情况图片
2)函数的原型

引入一个新的概念:所有的函数都有一个 prototype 的属性(注意:不是 _proto_)

new 关键字的步骤如下:

① 在内存中创建一个新的对象(空对象);

② 这个对象内部的 [[prototype]] 属性会被赋值为该构造函数的 prototype 属性;

③ 新的对象赋值给 this。

④ 执行代码。

⑤ 返回新对象。

那么也就意味着通过构造函数创建出来的所有对象的 [[prototype]] 属性都指向【构造函数.prototype】。

函数也是对象,也有 [[prototype]] 属性。

函数原型也叫显示原型。

3)原型对象

默认情况下原型对象上都会添加一个属性叫做 constructor,这个 constructor 指向当前的函数对象。

重写原型对象

javascript
function Person() {
}

console.log(Person.prototype) // { constructor: [Function: Person] }

// 在原有的原型对象上添加新的属性
// Person.prototype.message = "Hello Person"
// Person.prototype.info = { name: "哈哈哈", age: 30 }
// Person.prototype.running = function() {}

// console.log(Person.prototype)
// console.log(Object.keys(Person.prototype))

// 直接赋值一个新的原型对象
Person.prototype = {
  message: "Hello Person",
  info: { name: "哈哈哈", age: 30 },
  running: function() {},
  // constructor: Person
}
Object.defineProperty(Person.prototype, "constructor", {
  enumerable: false,
  configurable: true,
  writable: true,
  value: Person
})

console.log(Object.keys(Person.prototype))

// 新建实例对象
var p1 = new Person()
console.log(p1.message)

4. 原型链

javascript
var obj = {
  name: "why",
  age: 18
}

obj.__proto__ = {
}

obj.__proto__.__proto__ = {
}

obj.__proto__.__proto__.__proto__ = {
  address: "北京市"
}

Object 是所有类的父类

原型链最顶层的原型对象就是 Object 的原型对象。得出如下结论。

虽然可以说 Object 是所有对象的“父类”,但这并不完全准确,更准确的说法是 Object.prototype 是所有对象的原型。

javascript
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.running = function() {
  console.log(this.name + " is running~");
}

var pl = new Person("why", 18);

Object.prototype 方法补充

方法

isPrototypeOf:用于测试一个对象是否存在于另一个对象的原型链上。如果一个对象 A 存在于另一个对象 B 的原型链上,那么 A.isPrototypeOf(B) 将返回 true,否则返回 false。

instanceof:用于检测构造函数的 pototype 是否出现在某个实例对象的原型链上。

javascript
objectA.isPrototypeOf(objectB)  // 检查objectA是否在objectB的原型链上
objectB instanceof ConstructorA  // 检查ConstructorA.prototype是否在objectB的原型链上

区别

参数类型:

  • isPrototypeOf:两个参数都是对象。
  • instanceof:左侧是对象,右侧必须是构造函数。

检查方式:

  • isPrototypeOf:直接检查对象是否在另一个对象的原型链上。
  • instanceof:检查构造函数的 prototype 属性是否在对象的原型链上。
javascript
function Animal() {}
function Dog() {}
Dog.prototype = Object.create(Animal.prototype);

const dog = new Dog();

// instanceof检查
console.log(dog instanceof Dog);     // true
console.log(dog instanceof Animal);  // true

// isPrototypeOf检查
console.log(Dog.prototype.isPrototypeOf(dog));     // true
console.log(Animal.prototype.isPrototypeOf(dog));  // true

// 关键差异
console.log(Dog.isPrototypeOf(dog));           // false - 构造函数本身不在原型链上
console.log(dog instanceof Dog.prototype);     // 错误 - instanceof右侧需要是构造函数

三、继承

1. 实现

JavaScript 早期没有继承的关键字,如果需要实现继承,需要我们自己解决。原理是通过原型链来实现继承。

需求:学生类继承 Person 类。

版本一:共享原型
javascript
// 定义Person构造函数(类)
function Person(name, age, height, address) {
  this.name = name
  this.age = age
  this.height = height
  this.address = address
}

Person.prototype.running = function() {
  console.log("running~")
}
Person.prototype.eating = function() {
  console.log("eating~")
}

// 定义学生类
function Student(name, age, height, address, sno, score) {
  // 版本一、二:不能省略  --> 与 Person 类代码重复。缺陷!!!
  this.name = name
  this.age = age
  this.height = height
  this.address = address

  this.sno = sno
  this.score = score
}

// 父类的原型直接赋值给子类的原型
// 缺点: 父类和子类共享同一个原型对象, 修改了任意一个, 另外一个也被修改
Student.prototype = Person.prototype
版本二:原型链继承

通过让子类型的原型对象指向一个超类型的实例,从而实现子类型对超类型属性和方法的继承。

javascript
// 创建一个父类的实例对象(new Person()), 用这个实例对象来作为子类的原型对象
var p = new Person("why", 18)
Student.prototype = p

Student.prototype.studying = function() {
  console.log("studying~")
}

这样就实现了子类能够使用父类方法了。

javascript
var stu1 = new Student("kobe", 30, 111, 100)
var stu2 = new Student("james", 25, 111, 100)
stu1.running()
stu1.studying()

由于继承的属性和方法是在原型上,所以它们是所有实例共享的。这意味着,如果你改变了一个实例的继承属性,那么其他实例的这个属性也会被改变。此外,原型链继承不能实现参数的传递,这在某些情况下可能会造成问题。

版本三:借用构造函数继承

借用构造函数继承,也被称为经典继承。这种模式的主要思想是在子类型构造函数中通过调用超类型构造函数来实现继承。

优点是可以在子类型构造函数中向超类型构造函数传递参数;缺点是方法都在构造函数中定义,每次创建实例都会创建一遍方法。

javascript
function SuperType(name) {
  this.name = name;
  // 在构造函数中定义方法
  this.sayName = function() {
    console.log(this.name);
  };
}

function SubType(name, age) {
  // 借用构造函数继承
  SuperType.call(this, name);
  
  this.age = age;
}

var instance1 = new SubType("Alice", 25);
var instance2 = new SubType("Bob", 23);

// 这两个函数虽然功能相同,但是它们是不同的函数对象
console.log(instance1.sayName === instance2.sayName); // 输出 "false"
版本四:组合借用继承

组合借用继承也被称为伪经典继承或组合继承,是 JS 中常用的一种继承模式。这种模式结合了原型链继承和借用构造函数继承的优点。

原型链继承的优点是可以实现函数复用,缺点是所有实例共享原型中的属性;借用构造函数继承的优点是可以在子类中向超类传递参数,缺点是方法都在构造函数中定义,无法复用。

组合借用继承通过使用原型链来继承共享的属性和方法,然后通过借用构造函数来继承实例属性。这样,每个实例都会有自己的实例属性的副本,但仍然可以共享原型中的属性和方法。

javascript
function Student(name, age, height, address, sno, score) {
  // 重点: 借用构造函数
  Person.call(this, name, age, height, address)
  // this.name = name
  // this.age = age
  // this.height = height
  // this.address = address

  this.sno = sno
  this.score = score
}

组合继承存在什么问题呢?

组合继承最大的问题就是无论在什么情况下,都会调用两次父类构造函数。一次在创建子类原型的时候,另一次在子类构造函数内部。

版本五:寄生式继承

原型式继承函数

这种模式要从道格拉斯 · 克罗克福德(Douglas Crockford,著名的前端大师,JSON 的创立者)在 2006 年写的一篇文章说起:Prototypal Inheritance in JavaScript(在 JavaScript 中使用原型式继承)在这篇文章中,它介绍了一种继承方法,而且这种继承方法不是通过构造函数来实现的。

原型式继承是一种 JavaScript 继承模式。这种模式的基本思想是基于已有的对象创建新对象,同时还不必因此创建自定义类型。

为了实现这种继承模式,Douglas Crockford 提出了一个函数,叫做 Object.create()。这个函数接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。

javascript
var person = {
  name: "default",
  friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends); // Output: ["Shelby", "Court", "Van", "Rob", "Barbie"]

关键点是研究对象之间的继承。

javascript
var obj = {
  name:'jack',
  age:15,
}

var sub1 = {}
sub1._proto_ = obj

var sub2 = {}
sub2._proto_ = obj

寄生式继承函数

寄生式继承是与原型式继承紧密相关的一种思想,并且同样由道格拉斯 · 克罗克福德提出和推广的。

寄生式继承的思路是结合原型类继承和工厂模式的一种方式;即创建一个封装继承过程的函数 , 该函数在内部以某种方式来增强对象,最后再将这个对象返回。

javascript
function createAnother(original) {
  var clone = Object.create(original); // 通过调用函数创建一个新对象
  clone.sayHi = function() {  // 增强对象
    console.log("hi");
  };
  return clone;  // 返回这个对象
}

var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi();  // 输出 "hi"
版本六:寄生组合式继承

寄生组合式继承是一种 JavaScript 继承模式,它结合了寄生式继承和组合继承的优点,是实现基于类型继承的最有效方式。

寄生组合式继承的基本思想是:使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

javascript
// ------------------------------------------- 寄生组合式继承核心代码
function inheritPrototype(subType, superType) {
  var prototype = Object.create(superType.prototype); // 创建原型对象
  prototype.constructor = subType; // 增强原型对象
  subType.prototype = prototype; // 实现继承
}

// ------------------------------------------- 使用
function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() {
  console.log(this.name);
};

function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function() {
  console.log(this.age);
};

var instance1 = new SubType("Bob", 23);
instance1.sayName(); // 输出 "Bob"
instance1.sayAge(); // 输出 23

兼容旧版浏览器写法。

javascript
// 定义object函数
function object(o) {
  function F() {}
  F.prototype = o
  return new F()
}

// 定义寄生式核心函数
function inheritPrototype(subType, superType) {
  subType.prototype = object(superType.prototype)
  subType.prototype.constructor = subType
}

inheritPrototype(Student, Person)

2. 原型继承关系图

每个构造函数都有一个表示原型 prototype 属性。prototype 属性的作用就是让该函数所实例化的对象们都可以找到公用的属性和方法,即 f1.__proto__ === Foo.prototype。此外,因为函数也是一种对象,所以函数也拥有 __proto__ 属性。

__proto__ 属性是对象所独有的。作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的 __proto__ 属性所指向的那个对象(父对象)里找,一直找,直到 __proto__ 属性的终点 null,再往上找就相当于在 null 上取值,会报错。通过 __proto__ 属性将对象连接起来的这条链路即我们所谓的原型链。

1)概念解释

构造函数有一个 prototype 属性,指向实例对象的原型对象。通过同一个构造函数实例化的多个对象具有相同的原型对象。经常使用原型对象来实现继承。

javascript
function Foo(){};
Foo.prototype.a = 1;
var f1 = new Foo;
var f2 = new Foo;

console.log(Foo.prototype.a); // 1
console.log(f1.a); // 1
console.log(f2.a); // 1

原型对象有一个 constructor 属性,指向该原型对象对应的构造函数。

javascript
function Foo(){};
console.log(Foo.prototype.constructor === Foo); // true

由于实例对象可以继承原型对象的属性,所以实例对象也拥有 constructor 属性,同样指向原型对象对应的构造函数。

javascript
function Foo(){};
var f1 = new Foo;
console.log(f1.constructor === Foo); // true

实例对象有一个 proto 属性,指向该实例对象对应的原型对象。

javascript
function Foo(){};
var f1 = new Foo;
console.log(f1.__proto__ === Foo.prototype); // true
2)进一步分析
javascript
function Foo(){};
var f1 = new Foo;
第一部分:Foo

实例对象 f1 是通过构造函数 Foo() 的 new 操作创建的。构造函数 Foo() 的原型对象是 Foo.prototype;实例对象 f1 通过 __proto__ 属性也指向原型对象 Foo.prototype。

javascript
function Foo(){};
var f1 = new Foo;
console.log(f1.__proto === Foo.prototype); // true

实例对象 f1 本身并没有 constructor 属性,但它可以继承原型对象 Foo.prototype 的 constructor 属性。

javascript
function Foo(){};
var f1 = new Foo;
console.log(Foo.prototype.constructor === Foo); // true
console.log(f1.constructor === Foo); // true
console.log(f1.hasOwnProperty('constructor')); // false

下图是实例对象 f1 的控制台效果。

第二部分:Object

Foo.prototype 是 f1 的原型对象,同时它也是实例对象。实际上,任何对象都可以看做是通过 Object() 构造函数的 new 操作实例化的对象。所以,Foo.prototype 作为实例对象,它的构造函数是 Object(),原型对象是 Object.prototype。相应地,构造函数 Object() 的 prototype 属性指向原型对象 Object.prototype;实例对象 Foo.prototype 的 proto 属性同样指向原型对象 Object.prototype。

javascript
function Foo(){};
var f1 = new Foo;
console.log(Foo.prototype.__proto__ === Object.prototype); // true

实例对象 Foo.prototype 本身具有 constructor 属性,所以它会覆盖继承自原型对象 Object.prototype 的 constructor 属性。

javascript
function Foo(){};
var f1 = new Foo;
console.log(Foo.prototype.constructor === Foo); // true
console.log(Object.prototype.constructor === Object); // true
console.log(Foo.prototype.hasOwnProperty('constructor')); // true

下图是实例对象 Foo.prototype 的控制台效果。

如果 Object.prototype 作为实例对象的话,其原型对象是什么,结果是 null。

javascript
console.log(Object.prototype.__proto__ === null); // true
第三部分:Function

前面已经介绍过,函数也是对象,只不过是具有特殊功能的对象而已。任何函数都可以看做是通过 Function() 构造函数的 new 操作实例化的结果。

如果把函数 Foo 当成实例对象的话,其构造函数是 Function(),其原型对象是 Function.prototype;类似地,函数 Object 的构造函数也是 Function(),其原型对象是 Function.prototype。

javascript
function Foo(){};
var f1 = new Foo;
console.log(Foo.__proto__ === Function.prototype); // true
console.log(Object.__proto__ === Function.prototype); // true

原型对象 Function.prototype 的 constructor 属性指向构造函数 Function();实例对象 Object 和 Foo 本身没有 constructor 属性,需要继承原型对象 Function.prototype 的 constructor 属性。

javascript
function Foo(){};
var f1 = new Foo;
console.log(Function.prototype.constructor === Function); // true
console.log(Foo.constructor === Function); // true
console.log(Foo.hasOwnProperty('constructor')); // false
console.log(Object.constructor === Function); // true
console.log(Object.hasOwnProperty('constructor')); // false

所有的函数都可以看成是构造函数 Function() 的 new 操作的实例化对象。那么,Function 可以看成是调用其自身的 new 操作的实例化的结果。所以,如果 Function 作为实例对象,其构造函数是 Function,其原型对象是 Function.prototype。

javascript
console.log(Function.__proto__ === Function.prototype); // true
console.log(Function.prototype.constructor === Function); // true
console.log(Function.prototype === Function.prototype); // true

如果 Function.prototype 作为实例对象的话,其原型对象是什么呢?和前面一样,所有的对象都可以看成是 Object() 构造函数的 new 操作的实例化结果。所以,Function.prototype 的原型对象是 Object.prototype,其原型函数是 Object()。

javascript
console.log(Function.prototype.__proto__ === Object.prototype); // true

第二部分介绍过,Object.prototype 的原型对象是 null。

javascript
console.log(Object.prototype.__proto__ === null); // true

总结

【1】函数 (Function 也是函数) 是 new Function 的结果,所以函数可以作为实例对象,其构造函数是 Function(),原型对象是 Function.prototype。

【2】对象 (函数也是对象) 是 new Object 的结果,所以对象可以作为实例对象,其构造函数是 Object(),原型对象是 Object.prototype。

四、ES6 类定义

1. 初体验

javascript
// ES5 实现
function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);
javascript
// ES6 实现
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

var p = new Point(1, 2);

注意,定义 toString() 方法的时候,前面不需要加上 function 这个关键字,直接把函数定义放进去了就可以了。另外,方法与方法之间不需要逗号分隔,加了会报错。

语法糖

ES6 的类,完全可以看作构造函数的另一种写法。

javascript
class Point {
  // ...
}

typeof Point // "function"
Point === Point.prototype.constructor // true

构造函数的 prototype 属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的 prototype 属性上面。

javascript
class Point {
  constructor() {
    // ...
  }

  toString() {
    // ...
  }

  toValue() {
    // ...
  }
}

// 等同于
Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};

因此,在类的实例上面调用方法,其实就是调用原型上的方法。

javascript
class B {}
const b = new B();

b.constructor === B.prototype.constructor // true

上面代码中,b 是 B 类的实例,它的 constructor() 方法就是 B 类原型的 constructor() 方法。

由于类的方法都定义在 prototype 对象上面,所以类的新方法可以添加在 prototype 对象上面。Object.assign() 方法可以很方便地一次向类添加多个方法。

javascript
class Point {
  constructor(){
    // ...
  }
}

Object.assign(Point.prototype, {
  toString(){},
  toValue(){}
});

prototype 对象的 constructor 属性,直接指向“类”的本身,这与 ES5 的行为是一致的。

javascript
Point.prototype.constructor === Point // true

另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable)。

javascript
class Point {
  constructor(x, y) {
    // ...
  }

  toString() {
    // ...
  }
}

Object.keys(Point.prototype) // []
Object.getOwnPropertyNames(Point.prototype) // ["constructor","toString"]

上面代码中,toString() 方法是 Point 类内部定义的方法,它是不可枚举的。这一点与 ES5 的行为不一致。

javascript
var Point = function (x, y) {
  // ...
};

Point.prototype.toString = function () {
  // ...
};

Object.keys(Point.prototype) // ["toString"]
Object.getOwnPropertyNames(Point.prototype) // ["constructor","toString"]

上面代码采用 ES5 的写法,toString() 方法就是可枚举的。

相同点

① 类即构造函数:ES6 的类本质上是构造函数的语法糖,typeof 检查类返回 "function"。

② 原型机制不变:类的所有方法都定义在 prototype 属性上,实例通过原型链调用方法。

③ constructor 关系:prototype 对象的 constructor 属性直接指向类本身,保持与 ES5 一致的行为。

区别

① 方法不可枚举:类内定义的方法默认不可枚举,这是与 ES5 构造函数的主要区别。

② 必须 new 调用:class 定义的类必须使用 new 调用,否则报错(TypeError: Class constructor Foo cannot be invoked without 'new')。构造函数不用 new 也可以执行。

2. 内部成员

类的属性和方法,除非显式定义在其本身(即定义在 this 对象上),否则都是定义在原型上(即定义在 class 上)。

1)constructor() 方法

constructor() 方法是类的默认方法,通过 new 命令生成对象实例时,自动调用该方法。一个类必须有 constructor() 方法,如果没有显式定义,一个空的 constructor() 方法会被默认添加。

constructor() 方法默认返回实例对象(即 this),完全可以指定返回另外一个对象。

javascript
class Foo {
  constructor() {
    return Object.create(null);
  }
}

new Foo() instanceof Foo // false
2)实例属性的新写法

ES2022 为类的实例属性,又规定了一种新写法。实例属性现在除了可以定义在 constructor() 方法里面的 this 上面,也可以定义在类内部的最顶层。

javascript
class IncreasingCounter {
  _count = 0; // 实例属性

  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

注意:新写法定义的属性是实例对象自身的属性,而不是定义在实例对象的原型上面。

3)取值函数(getter)和存值函数(setter)
javascript
class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return 'getter';
  }
  set prop(value) {
    console.log('setter: '+value);
  }
}

let inst = new MyClass();
inst.prop = 123; // setter: 123
inst.prop // 'getter'

getter / setter 定义的属性在 MyClass.prototype 上。getter / setter 设置在属性的 Descriptor 对象上的。

javascript
// 等价于
function MyClass() {}

Object.defineProperty(MyClass.prototype, 'prop', {
  get: function() { return 'getter'; },
  set: function(value) { console.log('setter: ' + value); },
  enumerable: false,
  configurable: true
});
4)静态方法和静属性
[1] 静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

注意:

  • 如果静态方法包含 this 关键字,这个 this 指的是类,而不是实例。
  • 父类的静态方法,可以被子类继承。
  • 静态方法也是可以从 super 对象上调用的。
javascript
class Foo {
  static bar() {
    this.baz(); // 等价于 Foo.baz()
  }
  static baz() {
    console.log('hello');
  }
  baz() {
    console.log('world');
  }
}

var foo = new Foo();
foo.bar() // TypeError: foo.bar is not a function

Foo.bar() // 'hello'


class Bzz extends Foo {
  static baz() {
    return super.baz() + ', too';
  }
}
Bzz.bar() // 'hello'
Bzz.baz() // 'world, too'
[2] 静态属性

静态属性指的是 Class 本身的属性,即 Class.propName,而不是定义在实例对象(this)上的属性。

javascript
// 老写法
class Foo {
  // ...
}
Foo.prop = 1;

// 新写法
class Foo {
  static prop = 1;
}
5)私有方法和私有属性
[1] 早期解决方案

早期的 ES6 不提供,只能通过变通方法模拟实现。

命名约束

私有方法的名字前加上 _。

javascript
class Widget {
  // 公有方法
  foo (baz) {
    this._bar(baz);
  }

  // 私有方法
  _bar(baz) {
    return this.snaf = baz;
  }

  // ...
}

私有方法的名字命名为一个 Symbol 值。

javascript
const bar = Symbol('bar');
const snaf = Symbol('snaf');

export default class myClass{
  // 公有方法
  foo(baz) {
    this[bar](baz);
  }

  // 私有方法
  [bar](baz) {
    return this[snaf] = baz;
  }

  // ...
};

手动调用

索性将私有方法移出类。

javascript
class Widget {
  foo (baz) {
    bar.call(this, baz);
  }

  // ...
}

function bar(baz) {
  return this.snaf = baz;
}
[2] 正式写法

ES2022 正式为 class 添加了私有属性,方法是在属性名之前使用 # 表示。

注意:

  • 不管在类的内部或外部,读取一个不存在的私有属性,都会报错。这跟公开属性的行为完全不同,如果读取一个不存在的公开属性,不会报错,只会返回 undefined。

  • 私有属性的属性名必须包括 #,如果不带 #,会被当作另一个属性。例如:#xx 是两个不同的属性。

  • 私有属性也可以设置 getter 和 setter 方法。

  • 私有属性不限于从 this 引用,只要是在类的内部,实例也可以引用私有属性。

  • 私有属性和私有方法前面,也可以加上 static 关键字,表示这是一个静态的私有属性或私有方法。

  • 从 Chrome 111 开始,开发者工具里面可以读写私有属性,不会报错,原因是 Chrome 团队认为这样方便调试。

6)静态块

ES2022 引入了静态块(static block),允许在类的内部设置一个代码块,在类生成时运行且只运行一次,主要作用是对静态属性进行初始化。以后,新建类的实例时,这个块就不运行了。

javascript
class C {
  static x = ...;
  static y;
  static z;

  static {
    try {
      const obj = doSomethingWith(this.x);
      this.y = obj.y;
      this.z = obj.z;
    }
    catch {
      this.y = ...;
      this.z = ...;
    }
  }
}

注意:

  • 每个类允许有多个静态块,每个静态块中只能访问之前声明的静态属性。另外,静态块的内部不能有 return 语句。
  • 静态块内部可以使用类名或 this,指代当前类。

除了静态属性的初始化,静态块还有一个作用,就是将私有属性与类的外部代码分享。

javascript
let getX;

export class C {
  #x = 1;
  static {
    getX = obj => obj.#x;
  }
}

console.log(getX(new C())); // 1

3. in 运算符

直接访问某个类不存在的私有属性会报错,但是访问不存在的公开属性不会报错。这个特性可以用来判断,某个对象是否为类的实例。

javascript
class C {
  #brand;

  static isC(obj) {
    try {
      obj.#brand;
      return true;
    } catch {
      return false;
    }
  }
}

ES2022 改进了 in 运算符,使它也可以用来判断私有属性。

javascript
class C {
  #brand;

  static isC(obj) {
    if (#brand in obj) {
      // 私有属性 #brand 存在
      return true;
    } else {
      // 私有属性 #foo 不存在
      return false;
    }
  }
}

注意,判断私有属性时,in 只能用在类的内部。另外,判断所针对的私有属性,一定要先声明,否则会报错。

4. Class 表达式

javascript
// Me 只在 Class 的内部可用。在 Class 外部,这个类只能用 MyClass 引用。
const MyClass = class Me {
  getClassName() {
    return Me.name;
  }
};

// 如果类的内部没用到的话,可以省略 Me
const MyClass = class { /* ... */ };

立即执行的 Class

javascript
let person = new class {
  constructor(name) {
    this.name = name;
  }

  sayName() {
    console.log(this.name);
  }
}('张三');

person.sayName(); // "张三"

5. 类的注意点

类和模块的内部,默认就是严格模式。

不存在提升

第五章:DOM 操作

学习如何使用 JavaScript 操作 HTML 文档对象模型(DOM)来改变网页内容。

一、是什么?

DOM(Document Object Model)是文档对象模型。它是一种编程接口,用于 HTML 和 XML 文档。DOM 将文档表示为树形结构,其中每个节点都是对象,代表部分文档。

在 HTML 中,DOM 模型是由浏览器创建的,它将所有的 HTML 元素转换为对象,这样我们就可以使用 JavaScript 来操作这些对象,改变文档的结构、样式和内容。DOM 是完全平台和语言无关的接口,它允许程序和脚本动态地访问和更新文档的内容、结构和样式。

在 JavaScript 的 DOM 中,document 和 element 是两种不同类型的对象,它们各自代表 HTML 文档中的不同部分。

document 对象表示整个 HTML 文档。它可以被视为 DOM 树的根节点,提供了许多方法和属性,用于创建新的节点、查找节点、改变文档的结构等。例如,document.getElementById(id) 可以用于查找具有特定 ID 的元素,document.createElement(tagName) 可以用于创建一个新的元素。

element 对象代表HTML文档中的一个元素。每个HTML标签(如 <div><p><a> 等)在 DOM 中都会被表示为一个 element 对象。element 对象有自己的方法和属性,可以用于访问和修改元素的内容、属性、样式等。例如,element.innerHTML 可以用于获取或设置元素的 HTML 内容,element.setAttribute(name, value) 可以用于设置元素的属性。

简单来说,document 是整个 HTML 文档的代表,而 element 是文档中的一个单独元素的代表。你可以通过 document 对象来查找、创建和操作 element 对象,然后通过 element 对象来操作具体的 HTML 元素。

二、获取元素对象

1. 顶层元素

对于最顶层的 html 、head 、body 元素,我们可以直接在 document 对象中获取到:

文档声明 :<!DOCTYPE html> = document.doctype

html 元素 :<html> = document.documentElement

head 元素 :<head> = document.head

body 元素 :<body> = document.body

title 元素:<title> = document.title

2. 导航

1)节点之间的导航

如果获取到 一个节点后,可以根据这个节点去获取其他的节点。我们称之为节点之间的导航 。

节点之间存在如下的关系:

  • 父节点:parentNode
  • 前兄弟节点:previousSibling
  • 后兄弟节点:nextSibling
  • 所有子节点:childNodes
  • 第一个子节点:firstChild
  • 最后一个子节点:lastChild

Node 接口上定义的。

2)元素之间的导航

如果我们获取到一个元素后,可以根据这个元素去获取其他的元素,我们称之为元素之间的导航。

节点之间存在如下的关系:

  • 父元素:parentElement
  • 前兄弟节点:previousElementSibling
  • 后兄弟节点:nextElementSibling
  • 所有子元素:children
  • 第一个子元素:firstElementChild
  • 最后一个子元素:lastElementChild

HTMLElement 接口上定义的。

3)表格元素的导航

<table> 元素支持除了上面给出的导航,还可以使用以下这些属性:

  • table.rows —— <tr> 元素的集合;
  • table.caption / tHead / tFoot —— 引用元素 <caption><thead><tfoot>
  • table.tBodies —— <tbody> 元素的集合。

<thead><tfoot><tbody> 元素提供了 rows 属性:

  • tbody.rows —— 表格内部 <tr> 元素的集合;

<tr>

  • tr.cells —— 在给定 <tr> 中的 <td><th> 单元格的集合;
  • tr.sectionRowIndex —— 给定的 <tr> 在封闭的 <thead><tfoot><tbody> 中的位置(索引);
  • tr.rowIndex —— 在整个表格中 <tr> 的编号(包括表格的所有行);

<td><th>

  • td.cellIndex —— 在封闭的 <tr> 中单元格的编号。

HTMLTableElement 接口的属性。

4)表单元素的导航

<form> 元素可以直接通过 document 来获取:document.forms

<form> 元素中的内容可以通过 elements 来获取:form.elements。甚至还可以设置表单子元素的 name 来获取它们。

> Document 接口和 HTMLFormElement 接口的一部分。

3. 获取任意元素

方法名搜索方式可以在元素上调用?实时的?
querySelectorCSS-selector
querySelectorAllCSS-selector
getElementByIdid
getElementsByNamename 属性
getElementsByTagNametag or '*'
getElementsByClassNameclass

目前最常用的是 querySelector 和 querySelectAll。getElementById 偶尔也会使用或者在适配一些低版本浏览器时。

querySelector、querySelectorAll、getElementsByClassName、getElementsByTagName 方法是在 Document 接口和 Element 接口上定义的。这意味着你可以在整个文档上,或者在特定元素上使用这个方法。

getElementById、getElementsByName 方法是在 Document 接口。

三、操作元素对象

1. 元信息获取

nodeType

nodeType 属性提供了一种获取节点类型的方法,它是一个数值型值。常见的节点类型有如下:

常量描述
Node.ELEMENT_NODE1一个元素节点,例如 <p><div>
Node.TEXT_NODE3Element 或者 Attr 中实际的文字。
Node.COMMENT_NODE8一个 Comment 节点。
Node.DOCUMENT_NODE9一个 Document 节点。
Node.DOCUMENT_TYPE_NODE10描述文档类型的 DocumentType 节点。例如 <!DOCTYPE html> 就是用于 HTML5 的。

Node 接口中的属性。

nodeName、tagName

  • nodeName:这是 Node 接口的一个属性,它返回节点的名称。对于元素节点,nodeName 的值是元素的标签名。对于属性节点,nodeName 的值是属性的名称。对于文本节点,nodeName 的值是 "#text"。对于文档节点,nodeName 的值是 "#document"。

  • tagName:这是 Element 接口的一个属性,它返回元素的标签名。tagName 只在元素节点上可用,如果你尝试在非元素节点上访问 tagName,将会得到 undefined。

tagName 和 nodeName 之间有什么不同呢?

  • tagName 属性仅适用于 Element 节点;nodeName 是为任意 Node 定义的。
  • 对于元素,nodeName 的意义与 tagName 相同,所以使用哪一个都是可以的;对于其他节点类型(text、comment 等),它拥有一个对应节点类型的字符串;

innerHTML、outerHTML、innerText、textContent

1)是什么

innerHTML:获取或设置元素的 HTML 内容。获取时,会返回元素的所有子节点的 HTML,包括元素标签、属性和文本内容。当设置时,会解析提供的字符串作为 HTML,然后用这个 HTML 替换元素的所有子节点。

  • 解析 HTML 标签
  • 不包含元素自身标签,设置时不会替换元素本身
  • 有 XSS 风险
  • 会触发 DOM 重新解析

outerHTML 属性:获取时,会返回元素的开头和结束标签,以及元素的所有子节点的 HTML。当设置时,会解析提供的字符串作为 HTML,然后用这个 HTML 替换元素本身,而不仅仅是元素的子节点。

  • 解析 HTML 标签
  • 包含元素自身标,且设置时会替换元素本身
  • 有 XSS 风险
  • 会触发 DOM 重新解析

textContent:获取或设置元素的文本内容。获取时,会返回元素的所有文本节点的内容,不包括元素标签和属性。当设置时,会删除元素的所有子节点,然后创建一个新的文本节点,其内容是你提供的字符串。读取的值保留空格。

  • 不解析 HTML(标签会被转义)
  • 不受 CSS 影响(返回所有文本,包括隐藏元素的内容)
  • 性能最好(不触发重排)
  • 保留格式化空白符

innerText:读写内部的文本内容,会剔除掉标签。读取的值不保留空格。

  • 不解析 HTML(标签会被转义)
  • 受 CSS 影响(不返回 hidden 元素的内容)
  • 处理会触发重排(影响性能)
  • 不保留格式化空白符

2)textContent 和 innerText 区别

textContent 返回元素的所有文本内容,包括子元素中的文本。而 innerText 只返回被 CSS 渲染的文本内容,如果元素被 CSS 隐藏(例如,display: none),那么这个元素的文本内容不会被 innerText 返回。

textContent 保留文本中的所有空格和换行符,而 innerText 不保留多余的空格和换行符,它的行为更接近于浏览器渲染文本的方式。

html
<div id="example">
  Hello
  <span style="display: none;">World</span>
  <span>!</span>
</div>

<script>
  var element = document.getElementById('example');

  console.log(element.textContent); // 输出 "Hello/n  World/n !"
  console.log(element.innerText); // 输出 "Hello !"
</script>

innerHTML、outerHTML:这个属性是在 Element 接口中定义的。

textContent:这个属性是在 Node 接口中定义的。

nodeValue、data

nodeValue 用于获取非元素节点的文本内容。行为取决于节点的类型:

  • 对于 Element 节点(例如一个 <div><span> 元素),nodeValue 始终为 null。

  • 对于 Text 节点,nodeValue 是节点的文本内容。

  • 对于 Comment 节点,nodeValue 是注释的内容。

  • 对于 Attribute 节点,nodeValue 是属性的值。

data 是 CharacterData 接口的一个属性,它返回节点的数据。CharacterData 接口是 Text、Comment 和 CDATASection 接口的基接口,所以这些类型的节点都有 data 属性。对于这些类型的节点,data 的值是节点的文本内容。

javascript
let text = document.createTextNode('Hello, world!');
console.log(text.nodeValue); // 输出 "Hello, world!"
console.log(text.data); // 输出 "Hello, world!"

let attr = document.createAttribute('class');
attr.value = 'my-class';
console.log(attr.nodeValue); // 输出 "my-class"
console.log(attr.data); // 输出 "undefined"

let div = document.createElement('div');
console.log(div.nodeValue); // 输出 "null"
console.log(div.data); // 输出 "undefined"

注意:虽然 nodeValue 可以用来修改文本或注释节点的内容,但是通常推荐使用更具体的属性,如 textContent 或 value,因为它们的行为更直观,更易于理解。

hidden 属性

一个全局属性,可以用于设置元素隐藏。hidden 属性只是影响元素的可见性,它不会影响元素在 DOM 树中的位置。即使元素被隐藏,它仍然存在于 DOM 树中。

hidden 属性原理如下:

css
[hidden] {
  display: none;
}

如果你在元素上应用了一个将 display 属性设置为其他值的 CSS 规则,那么这个规则可能会覆盖 hidden 属性的效果,导致元素仍然可见。为了避免这种情况,你应该避免在具有 hidden 属性的元素上直接设置 display 属性。

DOM 元素还有其他属性

value:<input><select><textarea>(HTMLInputElement, HTMLSelectElement …………)的 value 。

href:<a href=''>(HTMLElement)的 href。

id:所有元素( HTMLElement )的 id” 特性( attribute )的值。

class 和 style 后续专门讲解的。

2. attribute 的操作

对于所有的 attribute 访问都支持如下的方法:

  • elem.hasAttribute(name) 检查特性是否存在。
  • elem.getAttribute(name) 获取这个特性值。
  • elem.setAttribute(name, value) 设置这个特性值。
  • elem.removeAttribute(name) 移除这个特性。

attribute 具备以下特征:

  • 它们的名字是大小写不敏感的(id 与 ID 相同)。
  • 它们的值总是字符串类型。

对于标准的 attribute,会在 DOM 对象上创建与其对应的 property 属性。

在大多数情况下,它们是相互作用的。改变 property,通过 attribute 获取的值,会随着改变;通过 attribute 操作修改,property 的值会随着改变。

除非特别情况,大多数情况下,设置、获取 attribute,推荐使用 property 的方式,这是因为它默认情况下是有类型的。

自定义属性

3. 样式操作

className 和 classList

元素的 class attribute,对应的 property 并非叫 class ,而是 className。

这是因为 JavaScript 早期是不允许使用 class 这种关键字来作为对象的属性,所以 DOM 规范使用了 className。

虽然现在 JavaScript 已经没有这样的限制,但是并不推荐,并且依然在使用 className 这个名称;

如果我们需要添加或者移除单个的 class ,那么可以使用 classList 属性。

  • elem.classList 是一个特殊的对象:
    • elem.classList.add(class):添加一个类。
    • elem.classList.remove(class):移除类。
    • elem.classList.toggle(class):如果类不存在就添加类,存在就移除它。
    • elem.classList.contains(class):检查给定类,返回 true/false 。

classList 是可迭代对象,可以通过 for ... of 进行遍历。

元素的 style 属性

如果需要单独修改某一个 CSS 属性,那么可以通过 style 来操作:

对于多词属性 ,使用驼峰式 camelCase。如果我们将值设置为空字符串,那么会使用 CSS 的默认样式。

多个样式的写法,我们需要使用 cssText 属性。

不推荐这种用法,因为它会替换原来 style 属性的整个字符串;

getComputedStyle

如果需要读取样式:

  • 对于内联样式,是可以通过 style.* 的方式读取到的。
  • 对于 style 、css 文件中的样式,是读取不到的。

这个时候,可以通过 getComputedStyle 的全局函数来实现:

javascript
let div = document.querySelector('div');
let style = window.getComputedStyle(div);

console.log(style.color); // 输出 "rgb(0, 0, 0)"
console.log(style['font-size']); // 输出 "16px"

4. 元素的增删改查

前面使用过 document.write 方法写入一个元素。这种方式写起来非常便捷,但是对于复杂的内容、元素关系拼接并不方便。它是在早期没有 DOM 的时候使用的方案,目前依然被保留了下来。

那么目前我们想要插入一个元素,通常会按照如下步骤:

步骤一:创建一个元素。

步骤二:插入元素到 DOM 的某一个位置。

创建元素

document.createElement(Tag);

插入元素

新方法:

  • node.append(...nodes or strings): 在 node 的子节点列表的末尾添加一个或多个节点或字符串。如果参数是字符串,那么它会被转换为文本节点然后添加。如果参数是已经存在于 DOM 树中的节点,那么这个节点会被移动,而不是复制。
  • node.prepend(...nodes or strings): 在 node 的子节点列表的开头添加一个或多个节点或字符串。参数的处理方式与 append 相同。
  • node.before(...nodes or strings): 在 node 之前插入一个或多个节点或字符串。参数的处理方式与 append 相同。
  • node.after(...nodes or strings): 在 node 之后插入一个或多个节点或字符串。参数的处理方式与 append 相同。
  • node.replaceWith(...nodes or strings): 用一个或多个节点或字符串替换 node,会替换子元素。参数的处理方式与 append 相同。
javascript
let div = document.createElement('div');
let p = document.createElement('p');
p.textContent = 'Hello, world!';
div.append(p); // 在 div 的子节点列表的末尾添加 p

let span = document.createElement('span');
span.textContent = 'Goodbye, world!';
div.prepend(span); // 在 div 的子节点列表的开头添加 span

let strong = document.createElement('strong');
strong.textContent = 'Important!';
p.before(strong); // 在 p 之前插入 strong

let em = document.createElement('em');
em.textContent = 'Emphasized!';
p.after(em); // 在 p 之后插入 em

let mark = document.createElement('mark');
mark.textContent = 'Marked!';
p.replaceWith(mark); // 用 mark 替换 p
  • insertAdjacentHTML
    • beforebegin:在目标元素本身的前面。
    • afterbegin:在目标元素的第一个子元素之前(即:在目标元素内部的开始位置)。
    • beforeend:在目标元素的最后一个子元素之后(即:在目标元素内部的结束位置)。
    • afterend:在目标元素本身的后面。

例如,假设有以下 HTML:

html
<div id="example">Hello, world!</div>

可以使用 insertAdjacentHTML() 方法和这些位置参数来插入新的 HTML:

javascript
document.getElementById('example').insertAdjacentHTML('beforebegin', '<p>beforebegin</p>');
document.getElementById('example').insertAdjacentHTML('afterbegin', '<p>afterbegin</p>');
document.getElementById('example').insertAdjacentHTML('beforeend', '<p>beforeend</p>');
document.getElementById('example').insertAdjacentHTML('afterend', '<p>afterend</p>');

/*
  生成以下 HTML:
  <p>beforebegin</p>
  <div id="example">
    <p>afterbegin</p>
    Hello, world!
    <p>beforeend</p>
  </div>
  <p>afterend</p>
*/

旧方法:

  • parentElem.appendChild(node): 在 parentElem 的子节点列表的末尾添加一个节点。如果 node 已经存在于 DOM 树中,那么它会被移动,而不是复制。
  • parentElem.insertBefore(newNode, referenceNode): 在 parentElem 的子节点列表中,将 newNode 插入到 referenceNode 之前。如果 referenceNode 是 null,那么 newNode 会被插入到子节点列表的末尾。如果 newNode 已经存在于 DOM 树中,那么它会被移动,而不是复制。
  • parentElem.replaceChild(newNode, oldNode): 在 parentElem 的子节点列表中,用 newNode 替换 oldNode。如果 newNode 已经存在于 DOM 树中,那么它会被移动,而不是复制。
  • parentElem.removeChild(node): 从 parentElem 的子节点列表中移除一个节点。
javascript
let div = document.createElement('div');
let p = document.createElement('p');
p.textContent = 'Hello, world!';
div.appendChild(p); // 在 div 的子节点列表的末尾添加 p

let span = document.createElement('span');
span.textContent = 'Goodbye, world!';
div.insertBefore(span, p); // 在 div 的子节点列表中,将 span 插入到 p 之前

let strong = document.createElement('strong');
strong.textContent = 'Important!';
div.replaceChild(strong, p); // 在 div 的子节点列表中,用 strong 替换 p

div.removeChild(strong); // 从 div 的子节点列表中移除 strong

移除元素

移除元素可以调用元素本身的 remove 方法。

克隆元素

如果想要复制一个现有的元素,可以通过 cloneNode 方法。可以传入一个 Boolean 类型的值,来决定是否是深度克隆。深度克隆会克隆对应元素的子元素,否则不会。

cloneNode 方法是在 Node 接口上定义的。

5. 元素大小

第一组

clientWidth :contentWith + padding(不包含滚动条)

clientHeight :contentHeight + padding

clientTop :border-top 的宽度

clientLeft :border-left 的宽度

第二组

offsetWidth :元素完整的宽度

offsetHeight :元素完整的高度

offsetTop :距离父元素(定位元素)的 y,如果一直没有,就会参照视口。
简单理解是参照页面位置,与 getBoundingClientRect.top 形成鲜明对比。

offsetLeft :距离父元素(定位元素)的 x,如果一直没有,就会参照视口。
简单理解是参照页面位置,与 getBoundingClientRect.left 形成鲜明对比。

第三组

scrollWidth :获取元素宽度(client 加上溢出的部分)。

scrollHeight :元素内容的总高度,包括由于溢出导致的视图中不可见内容。即使这些内容不可见,scrollHeight 也会包括它们。如果元素没有垂直滚动条,或者元素的内容小于元素的高度,那么 scrollHeight 就等于元素的视口高度。

scrollTop :一个元素的 scrollTop 值是这个元素的内容顶部到它的视口可见内容的顶部的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop 值为 0。

scrollLeft :内容在元素中向左滚动的距离。

第四组

元素对象.getBoundingClientRect() 返回对象,对象包含元素的位置和尺寸信息,对象有如下属性:

  • 元素对象.getBoundingClientRect().width :同 offsetWidth。

  • 元素对象.getBoundingClientRect().height :同 offsetHeihgt。

  • left :读取元素在视口上到位置 x 坐标。

  • top :读取元素在视口上到位置 y 坐标。

  • x :同 left。

  • y :同 top。

  • right :元素右边(右边框)到视口的 x 坐标。

  • bottom :元素底部(下边框)到视口的 y 坐标。

从图中不难看出:left = xtop = yright = x + widthbottom = y + height

6. window 的大小、滚动

window 的 width 和 height

  • innerWidth、innerHeight:获取 window 窗口的宽度和高度(包含滚动条)。
  • outerWidth、outerHeight:获取 window 窗口的整个宽度和高度(包括调试工具、工具栏)。

获取视口的尺寸

javascript
// 会包括滚动条本身的宽度
window.innerWidth
window.innerHeight

// 不会包括滚动条本身的宽度 (推荐)
document.documentElement.clientWidth
document.documentElement.clientHeight

文档的 width / height

整个文档的 width/height,其中包括滚动出去的部分。

从理论上讲,由于根文档元素是 document.documentElement,并且它包围了所有内容,因此我们可以通过使用 documentElement.scrollWidth/scrollHeight 来测量文档的完整大小。

但是在该元素上,对于整个文档,这些属性均无法正常工作。在 Chrome / Safari / Opera 中,如果没有滚动条,documentElement.scrollHeight 甚至可能小于 documentElement.clientHeight!很奇怪,对吧!

为了可靠地获得完整的文档高度,我们应该采用以下这些属性的最大值:

javascript
let scrollHeight = Math.max(
  document.body.scrollHeight, document.documentElement.scrollHeight,
  document.body.offsetHeight, document.documentElement.offsetHeight,
  document.body.clientHeight, document.documentElement.clientHeight
);

alert('整个文档的高度,包括滚动出的部分:' + scrollHeight);

为什么这样?最好不要问。这些不一致来源于远古时代,而不是“聪明”的逻辑。

获得当前滚动

DOM 元素的当前滚动状态在其 scrollLeft / scrollTop 属性中。

对于文档滚动,在大多数浏览器中,可以使用 document.documentElement.scrollLeft / scrollTop,但在较旧的基于 WebKit 的浏览器中则不行,应该使用 document.body 而不是 document.documentElement

幸运的是,我们根本不必记住这些特性,因为我们可以从 window.pageXOffset/pageYOffset 中获取页面当前滚动信息:

javascript
alert('当前已从顶部滚动:' + window.pageYOffset);
alert('当前已从左侧滚动:' + window.pageXOffset);

这些属性是只读的。

我们也可以从 window 的 scrollX 和 scrollY 属性中获取滚动信息

由于历史原因,存在了这两种属性,但它们是一样的:

  • window.pageXOffsetwindow.scrollX 的别名。
  • window.pageYOffsetwindow.scrollY 的别名。

区别

document.documentElement.scrollTopdocument.body.scrollTop 都是用来获取或设置页面滚动距离的属性,但它们在不同的浏览器和模式下的行为可能会有所不同。

在 HTML 文档中,document.documentElement 指向 <html> 元素,而 document.body 指向 <body> 元素。

  • document.documentElement.scrollTop:在标准模式(也称为严格模式)下,这个属性返回 <html> 元素的滚动距离。在混杂模式(也称为怪异模式)下,这个属性可能返回 0,具体取决于浏览器的实现。
  • document.body.scrollTop:在混杂模式下,这个属性返回 <body> 元素的滚动距离。在标准模式下,这个属性可能返回 0,具体取决于浏览器的实现。

然而,由于历史原因,一些浏览器(如 Chrome 和 Safari)在标准模式下也允许使用 document.body.scrollTop,并且当使用 document.documentElement.scrollTop 时会返回 0。因此,为了兼容所有浏览器和模式,你可以使用以下代码来获取页面滚动距离:

js
var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;

标准模式和混杂模式

标准模式(也称为严格模式)和混杂模式(也称为怪异模式)是浏览器解析和渲染网页的两种不同方式。

  • 标准模式:在这种模式下,浏览器会按照 W3C 的 HTML 和 CSS 规范准确地解析和渲染网页。这种模式通常需要在文档的开头声明一个 DOCTYPE,例如 <!DOCTYPE html>,来告诉浏览器这是一个标准模式的文档。
  • 混杂模式:在这种模式下,浏览器会模仿旧版本浏览器的行为来解析和渲染网页,这可能会导致一些现代的 HTML 和 CSS 特性无法正确地工作。如果一个文档没有声明 DOCTYPE,或者声明了一个不被识别的 DOCTYPE,那么浏览器通常会使用混杂模式来解析和渲染这个文档。

window 的滚动位置:

  • scrollX:一个只读属性,X 轴滚动的位置(别名 pageXOffset)
  • scrollY:一个只读属性,Y 轴滚动的位置(别名 pageYOffset)
  • screenX:一个只读属性,表示浏览器窗口左上角相对于屏幕左上角的水平像素距离。这个值包括了窗口的边框、工具栏和滚动条。(别名 screenLeft)
  • screenY:一个只读属性,表示浏览器窗口左上角相对于屏幕左上角的垂直像素距离。这个值包括了窗口的边框、工具栏和滚动条。(别名 screenTop)

也有提供对应的滚动方法:

  • scrollBy(x, y) :用于将文档的滚动位置相对地移动一定的像素数。x 参数表示在水平方向上滚动的像素数,y 参数表示在垂直方向上滚动的像素数。如果 x 或 y 是正数,那么文档会向右或向下滚动;如果 x 或 y 是负数,那么文档会向左或向上滚动。scrollBy 是在原地修改文档的滚动位置的,它不会创建新的滚动位置。如果你滚动的位置超过了文档的最大滚动位置,那么文档会被自动滚动到最大滚动位置。同样,如果你滚动的位置小于 0,那么文档会被自动滚动到 0。

    scrollBy 方法还可以接受一个对象作为参数,这个对象可以有两个属性:top 和 left,分别表示垂直和水平方向上的滚动距离。

  • scrollTo(pageX, pageY) :用于将文档的滚动位置移动到指定的像素坐标。pageX 参数表示在水平方向上的像素坐标,pageY 参数表示在垂直方向上的像素坐标。如果 pageX 或 pageY 是正数,那么文档会滚动到右边或下边的位置;如果 pageX 或 pageY 是 0,那么文档会滚动到左边或顶部的位置。scrollTo 是设置文档的滚动位置,它不是在原地修改滚动位置。如果你设置的位置超过了文档的最大滚动位置,那么文档会被自动滚动到最大滚动位置。同样,如果你设置的位置小于 0,那么文档会被自动滚动到 0。

第六章:事件监听

一、绑定事件

在 JavaScript 中,有三种主要的方式可以用来监听事件:

① 内联事件处理器:这是最早的事件处理方式,通过在 HTML 元素中直接添加事件处理属性(如 onclick、onload 等)来设置。

html
<button onclick="alert('Clicked!')">Click Me</button>

② 元素对象的事件处理属性:可以直接给 HTML 元素对象的事件处理属性赋值一个函数。

javascript
const button = document.querySelector('button');
button.onclick = function() {
  alert('Clicked!');
};

③ addEventListener 方法:这是最灵活、最推荐的方式。可以给同一个元素添加多个同类型的事件处理器,而且可以更灵活地控制事件的冒泡和默认行为。

javascript
const button = document.querySelector('button');
button.addEventListener('click', function() {
  alert('Clicked!');
});

二、事件流

事件流描述的是从页面接收事件的顺序。在 Web 中,事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。

① 事件捕获阶段:事件开始时,浏览器会在文档的根节点从上到下向目标元素进行事件传递的过程。

② 处于目标阶段:捕获阶段后,事件到达实际的目标元素。在这个阶段,如果定义了事件处理程序,那么事件就会被处理。

③ 事件冒泡阶段:事件完成后,会从目标元素开始,逐级向上回到文档的根节点。这个过程就是事件冒泡。

在使用事件时,可以选择在哪个阶段处理事件。在调用 addEventListener 方法时,将第三个参数设置为 true,那么事件处理程序将在捕获阶段调用;如果设置为 false 或者不设置,那么事件处理程序将在冒泡阶段调用。

假设有一个嵌套的元素结构,如下:

html
<div id="parent">
  父元素
  <button id="child">子元素</button>
</div>

可以为这两个元素添加事件监听器,如下:

javascript
const parent = document.getElementById('parent');
const child = document.getElementById('child');

parent.addEventListener('click', function() {
  console.log('父元素被点击');
}, true); // 在捕获阶段处理事件

child.addEventListener('click', function(event) {
  console.log('子元素被点击');
}); // 在冒泡阶段处理事件

现在,当你点击"子元素"按钮时,会发生以下事情:

① 事件开始在 document 节点,然后向下传递到 parent 元素。因为我们在捕获阶段添加了一个事件处理程序,所以会首先打印"父元素被点击"。

② 事件继续传递到 child 元素,这是事件的目标阶段。会打印"子元素被点击"。

③ 事件继续冒泡到 parent 元素和 document 节点,但是因为我们没有在冒泡阶段添加事件处理程序,所以不会有任何操作。

所以,最终的输出顺序是"父元素被点击",然后是"子元素被点击"。这就是事件流的工作方式。

三、事件对象

当一个事件发生时,浏览器会创建一个事件对象,这个对象包含了与事件相关的所有元信息。这个事件对象作为参数会传递给事件处理程序。

常见的属性:

  • type:事件的类型(如 "click", "load", "mouseover" 等)。
  • target:触发事件的元素。
  • currentTarget:绑定事件处理函数的元素(监听器所在元素)。
  • eventPhase:事件所处的阶段。
  • offsetX、offsetY:事件发生在元素内的位置。
  • clientX、clientY:事件发生在客户端内(浏览器视口 viewport)的位置。
  • pageX、pageY:事件发生在客户端相对于 document 的位置。
  • screenX、screenY:事件发生相对于屏幕的位置。

常见的方法:

  • preventDefault():一个方法,调用它可以阻止事件的默认行为(如果该事件有默认行为)。
  • stopPropagation():一个方法,调用它可以阻止事件的进一步传播,包括冒泡和捕获。

以下是一个使用事件对象的例子:

javascript
const button = document.querySelector('button');
button.addEventListener('click', function(event) {
  console.log('事件类型:', event.type); // 输出 "事件类型:click"
  console.log('触发事件的元素:', event.target); // 输出触发事件的元素对象
  event.preventDefault(); // 阻止事件的默认行为
});

在这个例子中,当点击按钮时,事件处理程序会接收到一个事件对象,然后输出事件的类型和触发事件的元素,并阻止事件的默认行为。

四、EventTarget 类

Window 也继承自 EventTarget。那么这个 EventTarget 是什么呢?

EventTarget 是一个 DOM 接口,主要用于添加、删除、派发 Event 事件。

EventTarget 常见的方法:

  • addEventListener:注册某个事件类型以及事件处理函数。

  • removeEventListener:移除某个事件类型以及事件处理函数。

  • dispatchEvent:派发某个事件类型到 EventTarget 上。

    javascript
    // 创建一个自定义事件
    const myEvent = new CustomEvent('myEvent', {
      detail: {
        message: '这是一个自定义事件'
      }
    });
    
    // 添加一个事件处理程序
    document.addEventListener('myEvent', function(e) {
      console.log(e.detail.message); // 输出 "这是一个自定义事件"
    });
    
    // 触发自定义事件
    document.dispatchEvent(myEvent);

五、常见的事件

1. 鼠标事件

属性描述
click当用户点击某个对象时调用的事件句柄。
contextmenu在用户点击鼠标右键打开上下文菜单时触发。
dblclick当用户双击某个对象时调用的事件句柄。
mousedown当用户在元素上按下鼠标按钮时触发。
mouseup当用户在元素上释放鼠标按钮时触发。
mouseover鼠标移到某元素之上。(支持冒泡)
mouseout鼠标从某元素移开。(支持冒泡)
mouseenter当鼠标指针移动到元素上时触发。(不支持冒泡)
mouseleave当鼠标指针移出元素时触发。(不支持冒泡)
mousemove鼠标被移动。

2. 键盘事件

属性描述
onkeydown当用户按下任何键盘按键(包括功能键如 Ctrl、Shift、Esc 等)时触发。
onkeypress当用户按下可以产生字符的按键(如字母键、数字键等)时触发。功能键(如 Ctrl、Shift、Esc 等)不会触发 onkeypress 事件。
onkeyup某个键盘按键被松开。

3. 表单事件

属性描述
onchange该事件在表单元素的内容改变时触发(<input>, <keygen>, <select>, <textarea>)。
若监听输入框元素,不仅要输入框内容改变,还要失去焦点才会触发。
若监听选择框元素,一改变就触发。
oninput元素获取用户输入时触发,也就是输入框内容改变就会触发。需要监听到输入框或文本域元素上。
onfocus元素获取焦点时触发。需要监听到表单控件上。
onblur元素失去焦点时触发。需要监听到表单控件上。
onreset表单重置时触发。需要监听到 form 元素上。
onsubmit表单提交时触发。需要监听到 form 元素上。

4. 文档加载事件

DOMContentLoaded:浏览器已完全加载 HTML,并构建了 DOM 树,但像 <img> 和样式表之类的外部资源可能尚未加载完成。

load:浏览器不仅加载完成了 HTML,还加载完成了所有外部资源(图片、样式等)。

5. 图片事件

load 事件:当图像已经完成加载时,会触发 load 事件。可以使用这个事件来执行一些操作,比如显示图像或执行其他的 JavaScript 代码。

error 事件:如果图像加载失败(例如,由于图像文件不存在或网络问题),会触发 error 事件。可以使用这个事件来显示一个备用图像或显示错误消息。

6. 过渡事件

事件名功能介绍
transitionstart过渡开始事件,在过渡延迟之后触发
transitionend过渡结束事件
transitionrun过渡开始事件,在过渡延迟之前触发

事件触发顺序通常如下:

① transitionrun:过渡开始时立即触发。

② transitionstart:过渡实际开始时触发(如果有延迟,会在延迟结束后触发)。

③ transitionend:过渡结束时触发。

即使没有延迟,这三个事件也会按照这个顺序触发,只是 transitionrun 和 transitionstart 之间的时间间隔会非常短,几乎是同时发生的。

7. 动画事件

事件名功能介绍
animationstart动画开始事件
animationend动画最终结束事件
animationiteration该事件动画每重复执行一次就触发一次

8. 其他事件

事件名功能介绍
scroll内容发生滚动,需要监听给有滚动条的元素或者 window
resize视口大小发生变化,需要监听给 window

第七章:BOM

BOM(Browser Object Model,浏览器对象模型)是描述浏览器功能的模型。它提供了与浏览器窗口进行交互的对象和方法。BOM 是 JavaScript 的核心之一,但并没有正式的标准和定义。

BOM 主要包括以下对象:

① Screen 对象:包含有关客户端显示屏幕的信息。

② Navigator 对象:包含有关浏览器的信息。

③ Window 对象:代表浏览器窗口或一个框架,它是 BOM 的顶级对象,所有其他 BOM 对象都是 Window 对象的属性。

④ Location 对象:包含有关当前 URL 的信息,并提供方法来重定向浏览器到新的 URL。

⑤ History 对象:包含用户(在浏览器窗口中)访问过的 URL。

BOM 可以看成是连接 JavaScript 脚本与浏览器窗口的桥梁。

一、window 对象

window 对象在浏览器中可以从两个视角来看待:

  • 视角一:全局对象。

    我们知道 ECMAScript 其实是有一个全局对象的,这个全局对象在 Node 中是 global。

    在浏览器中就是 window 对象。

  • 视角二:浏览器窗口对象。

    作为浏览器窗口时,提供了对浏览器操作的相关的 API。

当然,这两个视角存在大量重叠的地方,所以不需要刻意去区分它们:

  • 对于浏览器和 Node 中全局对象名称不一样的情况,目前已经指定了对应的标准,称之为 globalThis,并且大多数现代 浏览器都支持它。
  • 放在 window 对象上的所有属性都可以被访问。
  • 使用 var 定义的变量会被添加到 window 对象中。

window 默认给我们提供了全局的函数和类(setTimeout 、Math 、Date 、Object 等)。window 对象上肩负的重担是非常大的:

第一:包含大量的属性 localStorage、console、location、history、screenX、scrollX 等等(大概 60+ 个属性)。

第二:包含大量的方法 alert、close、scrollTo、open 等等(大概 40+ 个方法)。

第三:包含大量的事件 focus、blur、load、hashchange 等等(大概 30+ 个事件)。

第四:包含从 EventTarget 继承过来的方法 addEventListener、removeEventListener、dispatchEvent 方法。

二、location

location 对象用于表示 window 上当前链接到的 URL 信息。

常见的属性

  • href:当前 window 对应的超链接 URL,整个 URL。

    javascript
    window.location.href="https://www.baidu.com"; // 在同当前窗口中打开窗口
    window.open("https://www.baidu.com", "_self"); // 在同当前窗口中打开窗口
  • protocol:当前的协议。

  • host:主机地址,带端口。

  • hostname:主机地址,不带端口。

  • port:端口。

  • pathname:路径。

  • search:查询字符串。

  • hash:哈希值。

  • username:URL 中的 username(很多浏览器已经禁用)。

    http://username:password@example.com

  • password:URL 中的 password(很多浏览器已经禁用)。

常用的方法

  • assign:赋值一个新的 URL,并且跳转到该 URL 中。
  • replace:打开一个新的 URL,并且跳转到该 URL 中(不同的是不会在浏览记录中留下之前的记录)。
  • reload:重新加载页面,可以传入一个 Boolean 类型。传入的参数是 false,那么页面会尝试从浏览器缓存中加载,这被称为“软重载”。为 true 时,页面会强制从服务器加载,忽略浏览器缓存,这被称为“硬重载”。

URLSearchParams

URLSearchParams 定义了一些实用的方法来处理 URL 的查询字符串。可以将一个字符串转化成 URLSearchParams 类型,也可以将一个 URLSearchParams 类型转成字符串。

常见的方法:

  • append(name, value):在查询字符串中添加一个新的键值对。
  • delete(name):从查询字符串中删除指定的键值对。
  • get(name):从查询字符串中获取指定键的第一个值。
  • getAll(name):从查询字符串中获取指定键的所有值。
  • has(name):检查查询字符串中是否存在指定的键。
  • set(name, value):在查询字符串中设置指定键的值。
  • toString():转为字符串。
javascript
let params = new URLSearchParams('foo=1&foo=2&bar=3');
console.log(params.get('bar'));  // 输出 "3"
console.log(params.getAll('foo'));  // 输出 ["1", "2"]

encodeURIComponent 和 decodeURIComponent

编码和解码中文字符。

encodeURIComponent 方法可以将中文字符编码为 URI 组件,这样它们就可以安全地用在 URL 中了。

javascript
let url = "http://example.com/?name=" + encodeURIComponent("张三");
console.log(url);  // 输出 "http://example.com/?name=%E5%BC%A0%E4%B8%89"

decodeURIComponent 方法可以将使用 encodeURIComponent 方法编码的中文字符解码回来。

javascript
let name = decodeURIComponent("%E5%BC%A0%E4%B8%89");
console.log(name);  // 输出 "张三"

三、history

history 对象允许我们访问浏览器曾经的会话历史记录。

常见的属性

  • length:会话中的记录条数。
  • state:当前保留的状态值。

常用的方法

  • back():返回上一页,等价于 history.go(-1)

  • forward():前进下一页,等价于 history.go(1)

  • go():加载历史中的某一页。

  • pushState(状态对象, 标题, URL):打开一个指定的地址。

    javascript
    history.pushState({page: 1}, "title 1", "/home");
    console.log(history.state);  // 输出 {page: 1}
  • replaceState(状态对象, 标题, URL):打开一个新的地址,并且使用 replace。

history 和 hash 目前是 vue 、react 等框架实现路由的底层原理。

四、JSON

1. 是什么?由来?

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,它基于 JavaScript 的一个子集。JSON 使用人类可读的文本来表示数据对象,包含属性-值对或者序列化的值。它被用来存储信息或者在服务器和客户端之间进行数据传输。

JSON 的概念最早由 Douglas Crockford 提出。在 2001 年,他在一个在线讨论论坛上首次提出了 JSON 这个概念,并在 2002 年创建了一个网站来描述和推广这种数据格式。

尽管 JSON 的语法源自 JavaScript,但它已经成为一种独立的数据格式,并且被许多编程语言所支持。这主要是因为 JSON 具有简单、易读、易写和易解析的特性,使得它成为一种理想的数据交换格式。

2006 年,JSON 被 ECMAScript 正式采纳为其第五版的一部分。在 2013 年,JSON 被国际标准化组织(ISO)和国际电工委员会(IEC)采纳为国际标准。

如今,JSON 已经广泛应用于各种应用程序中,包括 web 应用、移动应用、APIs 等,它已经成为互联网上最常用的数据交换格式之一。

拓展:早期有 XML。目前发展中的有 Protobuf。

2. 语法

JSON 的顶层支持三种类型的值:

简单值:数字 Number 、字符串 String (不支持单引号) 、布尔类型 Boolean 、null。

对象值:由 key 、value 组成,key 是字符串类型,并且必须添加双引号,值可以是简单值、对象值、数组值。

数组值:数组的值可以是简单值、对象值、数组值。

JSON 要求最后一个键值对不要添加逗号。

3. JSON 序列化

在 ES5 中引用了 JSON 全局对象,该对象有两个常用的方法:

  • stringify 方法:将 JavaScript 类型转成对应的 JSON 字符串。
  • parse 方法:解析 JSON 字符串,转回对应的 JavaScript 类型(对象)。

可以使用 stringify 与 parse 实现深拷贝。

1)stringify 方法

方法接受三个参数:要转换的值,一个 replacer 函数或数组,以及一个用于插入空白的 space 参数。

replacer 函数:如果提供了 replacer 函数,那么在序列化过程中,对象所有的属性都会调用这个函数。函数的返回值将被用作属性的值。如果函数返回 undefined,那么这个属性将不会被序列化到最终的 JSON 字符串中。

javascript
let obj = {name: 'John', age: 30, city: 'New York'};

let json = JSON.stringify(obj, (key, value) => {
  if (typeof value === 'string') {
    return undefined;
  }
  return value;
});

console.log(json);  // 输出 {"age":30}

replacer 数组:如果 replacer 是一个数组,那么只有数组中指定的属性会被包含在序列化的 JSON 字符串中。

javascript
let obj = {name: 'John', age: 30, city: 'New York'};

let json = JSON.stringify(obj, ['name', 'city']);

console.log(json);  // 输出 {"name":"John","city":"New York"}

space 是一个数字:结果字符串会有相应数量的空格字符用于缩进。如果数字大于 10,那么缩进会被限制为 10 个空格。如果 space 是负数或者 0,那么不会有任何缩进。

javascript
let obj = {name: 'John', age: 30, city: 'New York'};
let json = JSON.stringify(obj, null, 4);
console.log(json);

/*
{
    "name": "John",
    "age": 30,
    "city": "New York"
}
*/

space 是一个字符串:那么这个字符串(或者它的前 10 个字符)会被用作缩进。

javascript
let obj = {name: 'John', age: 30, city: 'New York'};
let json = JSON.stringify(obj, null, '\t');
console.log(json);

/*
{
    "name": "John",
    "age": 30,
    "city": "New York"
}
*/
2)parse 方法

JSON.parse() 方法可以接受一个可选的 reviver 函数作为第二个参数。这个 reviver 函数允许你对解析生成的 JavaScript 值或对象进行进一步的处理。

reviver 函数接受两个参数:一个是键(key),一个是值(value)。每个键值对都会调用这个函数,如果函数返回 undefined,那么这个键值对会从最终的对象中删除。如果函数返回其他值,那么这个值会替换原来的值。

JSON.parse() 的第二个参数也可以是一个数组。这个数组用于指定要保留的属性名称。

javascript
let json = '{"name":"John", "age":30, "city":"New York"}';

let obj = JSON.parse(json, (key, value) => {
  if (typeof value === 'number') {
    return value * 2; // 将所有数字值翻倍
  }
  return value;
});

console.log(obj); // 输出 { name: 'John', age: 60, city: 'New York' }

JSON.parse(null):的结果依然是 null。

在 JSON 规范中,null 是一个有效的 JSON 值。所以当 JSON.parse() 接收到 null 时,它将其视为一个已经是有效 JSON 格式的值,不需要进一步解析,直接返回 null。

五、webStorage

是什么

localStorage:本地存储。提供的是一种永久性的存储方法,在关闭掉网页重新打开时,存储的内容依然保留。

sessionStorage:会话存储。提供的是本次会话的存储,在关闭掉会话时,存储的内容会被清除。

存储内容大小一般支持 5MB 左右(不同浏览器可能还不一样)。

sessionStorage、localStorage 跟 cookie 和 session 一点关系都没有。

localStorage 和 sessionStorage 的区别

关闭网页后重新打开,localStorage 会保留,而 sessionStorage 会被删除。

在页面内实现跳转,localStorage 会保留, sessionStorage 也会保留。

在页面外实现跳转(打开新的网页),localStorage 会保留,sessionStorage 不会被保留。

相关API

浏览器端通过 Window.sessionStorage 和 Window.localStorage 属性来实现本地存储机制。

xxxxxStorage.length:只读属性

返回一个整数,表示存储在 Storage 对象中的数据项数量;

xxxxxStorage.key():接受一个数值 n 作为参数,返回存储中的第 n 个 key 名称;n 是从 0 开始计数的。

xxxxxStorage.setItem('key', 'value');

该方法接受一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值。

xxxxxStorage.getItem('person');

该方法接受一个键名作为参数,返回键名对应的值。

xxxxxStorage.removeItem('key');

该方法接受一个键名作为参数,并把该键名从存储中删除。

xxxxxStorage.clear()

该方法会清空存储中的所有数据。

备注:xxxxxStorage.getItem(xxx):如果 xxx 对应的 value 获取不到,那么 getItem 的返回值是 null。

注意:

不管是本地存储还是会话存储,一般存储的是字符串,如果需要存储对象或者其他形式,需要先通过JSON.stringify(xxx)将其先转化为字符串,才能进行存储。如果需要拿到数据,则需要通过JSON.parse(xxx)使字符串转化为之前的数据类型。

第八章:ES6 - 13 新特性

一、ES5

1. 词法环境(Lexical Environments)

词法环境是一种规范类型,用于在词法嵌套结构中定义关联的变量、函数等标识符。

一个词法环境是由环境记录(Environment Record)和一个外部词法环境(outer Lexical Environment)组成。

一个词法环境经常用于关联一个函数声明、代码块语句、try-catch 语句,当它们的代码被执行时,词法环境被创建出来。

也就是在 ES5 之后,执行一个代码,通常会关联对应的词法环境。

那么执行上下文会关联哪些词法环境呢?

Lexical Environment 和 Variable Environment。Lexical Environment 用于处理 let、const 声明的标识符;Variable Environment 用于处理 var 和 function 声明的标识符。

2. 环境记录(Environment Record)

在这个规范中有两种主要的环境记录值声明式环境记录和对象环境记录。

声明式环境记录:声明性环境记录用于定义 ECMAScript 语言语法元素的效果,如函数声明、变量声明和直接将标识符绑定与 ECMAScript 语言值关联起来的 Catch 子句。

对象式环境记录:对象环境记录用于定义 ECMAScript 元素的效果,例如 WithStatement,它将标识符绑定与某些对象的属性关联起来。

3. 严格模式

严格模式可以让我们及早发现错误,使代码更安全规范,推荐在代码中一直保持严格模式运行。

主流框架都采用严格模式,严格模式也是未来 JS 标准,所以建议代码使用严格模式开发。

1)规则

全局变量声明

变量必须使用关键词声明,未声明的变量不允许赋值。

javascript
"use strict";
url = 'https://baidu.com'; // url is not defined

强制声明防止污染全局。

javascript
// "use strict";
function run() {
  name = "javascript";
}
run();
console.log(name); // javascript

非严格模式可以不使用声明指令,严格模式下必须使用声明。所以建议使用 let 等声明。

javascript
// "use strict";
({name,url} = {name:'雨下田上',url:'https://baidu.com'});
console.log(name, url);

重复参数名:变量参数不允许重复定义。

javascript
"use strict";
//不允许参数重名
function run(name, name) {}

this 的值:在严格模式下,this 在函数中未被显式设置时会是 undefined,而不是全局对象。

javascript
"use strict";
function logThis() {
  console.log(this); // undefined
}
logThis();

八进制字面量:严格模式下,八进制字面量(以 0 开头的数字)是无效的。

javascript
"use strict";
var num = 010; // 抛出错误:Octal literals are not allowed in strict mode.

with 语句:严格模式下,with 语句是无效的。

javascript
"use strict";
with (Math) { // 抛出错误:Strict mode code may not include a with statement
  x = cos(2);
}

eval:严格模式下,eval 不能引入新的变量到作用域中。

javascript
"use strict";
eval("var x = 2");
console.log(x); // 抛出错误:x is not defined

静默错误:一些在非严格模式下会静默失败的赋值操作,在严格模式下会抛出错误。

javascript
"use strict";
var obj = {};
Object.defineProperty(obj, "x", { value: 42, writable: false });
obj.x = 9; // 抛出错误:Cannot assign to read only property 'x' of object

删除不可删除的属性:严格模式下,尝试删除不可删除的属性会抛出错误。

javascript
"use strict";
delete Object.prototype; // 抛出错误:Cannot delete property 'prototype' of function Object() { [native code] }
2)范围

严格模式的作用域取决于启用它的位置。严格模式可以在整个脚本或单个函数中启用。

单独为函数设置严格模式。

javascript
function strict(){
  "use strict";
  url = 'https://baidu.com'; // 报错
  return "严格模式";
}

function notStrict() {
  url = 'https://baidu.com'; // 不报错
  return "正常模式";
}

为了在多文件合并时,防止全局设置严格模式对其他没使用严格模式文件的影响,一般都会将脚本放在一个立即执行函数中。

javascript
(function () {
  "use strict";
  url = 'https://baidu.com';
})();

二、ES6 新特性

已经学习过的:展开语法、解构赋值、函数的剩余参数、箭头函数、类(Class)、Map 和 Set 数据结构、Symbol 类型。

后面学习:模块(Module)、Promise。

推荐书籍:ECMAScript6入门

1. var & let & const

在定义变量时一定要写关键字,否则变为全局变量。

  • var 定义的变量会污染 window 对象的变量;let 不会。

  • var 可以重复声明,而 let 与 const 不行。

  • var 声明的变量是会进行变量提升的。let & const 不会。

    那么是不是意味着 let 声明的变量只有在代码执行阶段才会创建的呢?

    这些变量会被创建在包含他们的词法环境被实例化时,但是是不可以访问它们的,直到词法绑定被求值。从块作用域的顶部一直到变量声明完成之前,这个变量处在暂时性死区(TDZ,temporal dead zone)。使用术语 “temporal” 是因为区域取决于执行顺序(时间),而不是编写代码的位置。

  • var 没有块级作用域,let & const 有块级作用域。

  • const 在定义时必须赋值,且后续不能改变。

2. 字符串模板

字符串模板(也被称为模板字面量)是一种创建字符串的新方式,它提供了一种更方便的方式来嵌入变量和表达式。

字符串模板使用反引号来定义。在字符串模板中,可以直接嵌入变量或表达式,只需要将它们放在 ${} 中即可。

3. 标签模板字符串

允许通过一个函数来处理模板字符串。

标签模板字符串的语法是:一个函数名,后面紧跟一个模板字符串。例如:

javascript
let name = "张三";

function foo(...args){
  console.log(args);
}

console.log(foo("Hello world!")); // ["Hello world!"]
console.log(foo`Hello, ${name} ${name}!`); // [['Hello, ', ' ', '!'], '张三', '张三']

在这个例子中,foo 是一个标签函数,它会接收到两类参数:

① 字符串值数组:这个数组包含了模板字符串中的所有字符串值。在这个例子中,它是 ['Hello, ', ' ', '!']

② 替换值:这些值是模板字符串中 ${} 中的表达式的结果。在这个例子中,它是 '张三', '张三'

4. 函数的默认参数

js 中,形参可以有默认值,不要求在最后面。但是使用函数的 length 属性,不会计算有默认值的形参。

js
function greet(name = "World") {
  console.log(`Hello, ${name}!`);
}

greet();            // 输出 "Hello, World!"
greet("张三");       // 输出 "Hello, 张三!"

5. 数值的表示

在 ES6 中规范了二进制和八进制的写法:

  • 二进制数值,使用 0b0B 作为前缀。

  • 八进制数值,使用 0o0O 作为前缀。

另外,在 ES2021 中,还增加了数值分隔符(Numeric Separators)。可以在数字中使用下划线 _ 作为分隔符,以提高数字的可读性。

js
let billion = 1_000_000_000;  // 这是十亿
let bytes = 0x89_AB_CD_EF;    // 这是一个十六进制数值
let bits = 0b1101_0111;       // 这是一个二进制数值
let fraction = 0.123_456;     // 这是一个小数

6. Proxy 使用

在 ES6 中,新增了一个 Proxy 类,用于创建一个代理。

如果需要监听一个对象的相关操作,那么,可以先创建一个代理对象(Pr oxy 对象),之后对该对象的所有操作都通过代理对象来完成,代理对象可以监听我们对原对象进行了哪些操作。

1)基本使用

创建 new Proxy 对象,并且传入需要侦听的对象以及一个处理对象,可以称之为 handler。

javascript
const p = new Proxy(target, handler);

之后的操作都是直接对 Proxy 的操作,而不是原有的对象,因为我们需要在 handler 里面进行侦听。

2)捕获器

set 和 get 捕获器

  • get 捕获器接收以下参数:
    1. target:被代理的对象。
    2. property:要获取的属性名。
    3. receiver:通常是代理本身,但有时可能是继承链上的其他对象。
  • set 捕获器接收以下参数:
    1. target:被代理的对象。
    2. property:要设置的属性名。
    3. value:要设置的新属性值。
    4. receiver:通常是代理本身,但有时可能是继承链上的其他对象。
javascript
let obj = {
  a: 1,
  b: 2
};

let p = new Proxy(obj, {
  get: function(target, property, receiver) {
    console.log(`Getting ${property}`);
    return target[property];
  },
  set: function(target, property, value, receiver) {
    console.log(`Setting ${property} to ${value}`);
    target[property] = value;
  }
});

console.log(p.a);  // 输出:Getting a
                   // 输出:1

p.b = 3;           // 输出:Setting b to 3
console.log(p.b);  // 输出:Getting b
                   // 输出:3

Proxy 所有捕获器

  1. get(target, property, receiver): 用于拦截属性访问。
  2. set(target, property, value, receiver): 用于拦截属性赋值。
  3. has(target, property): 用于拦截 in 操作符。
  4. deleteProperty(target, property): 用于拦截 delete 操作符。
  5. apply(target, thisArg, argumentsList): 用于拦截函数调用。
  6. construct(target, argumentsList, newTarget): 用于拦截 new 操作符。
  7. ownKeys(target): 用于拦截 Object.getOwnPropertyNamesObject.getOwnPropertySymbols
  8. getOwnPropertyDescriptor(target, property): 用于拦截 Object.getOwnPropertyDescriptor
  9. defineProperty(target, property, descriptor): 用于拦截 Object.definePropertyObject.defineProperties
  10. getPrototypeOf(target): 用于拦截 Object.getPrototypeOf
  11. setPrototypeOf(target, prototype): 用于拦截 Object.setPrototypeOf
  12. preventExtensions(target): 用于拦截 Object.preventExtensions
  13. isExtensible(target): 用于拦截 Object.isExtensible

几个常用的捕获器使用

construct 是 JavaScript 中的一个 Proxy 捕获器,它用于拦截 new 操作符。当使用 new 操作符创建一个新对象时,construct 捕获器会被调用。

construct 捕获器接受三个参数:

  • target:目标对象,即被代理的构造函数。
  • argumentsList:一个包含所有构造函数参数的列表。
  • newTarget:最初被调用的构造函数。在直接调用时,通常是 target,在继承中可能是子类的构造函数。
javascript
function OneClass() {
  this.name = 'one'
}

function OtherClass() {
  this.name = 'other'
}
const args=[1, 2, 3];

let obj1 = Reflect.construct(OneClass, args, OtherClass) // 使用 OneClass 的构造函数来创建对象,但这个对象的原型是 OtherClass.prototype

// 创建一个对象 obj2,其原型是 OtherClass.prototype,然后使用 OneClass 的构造函数初始化这个对象
let obj2 = Object.create(OtherClass.prototype)
OneClass.apply(obj2, args)

console.log(obj1.name) // one
console.log(obj2.name) // one

console.log(obj1 instanceof OneClass) // false
console.log(obj2 instanceof OneClass) // false

console.log(obj1 instanceof OtherClass) // true
console.log(obj2 instanceof OtherClass) // true

捕捉器 construct 和 apply,它们是应用于函数对象的。

javascript
function sum(a, b) {
  return a + b;
}

let p = new Proxy(sum, {
  apply: function(target, thisArg, argumentsList) {
    console.log(`Calling function with arguments ${argumentsList}`);
    return Reflect.apply(target, thisArg, argumentsList);
  },
  construct: function(target, argumentsList, newTarget) {
    console.log(`Using new operator with arguments ${argumentsList}`);
    return Reflect.construct(target, argumentsList, newTarget);
  }
});

console.log(p(1, 2));  // 输出:Calling function with arguments 1,2
                       // 输出:3

console.log(new p(1, 2));  // 输出:Using new operator with arguments 1,2
                           // 输出:sum {}

7. Reflect

是什么

Reflect 是 ES6 新增的一个 API,它是 一个对象,字面的意思是反射。

那么这个 Reflect 有什么用呢?它主要提供了很多操作 JavaScript 对象的方法,有点像 Object 中操作对象的方法。

有 Object 可以做这些操作,那么为什么还需要有 Reflect 这样的新增对象呢?

① 使某些操作更加函数式:在 JavaScript 中,有些操作是以命令式的方式进行的,例如 delete obj.propxxx in XXX 等。Reflect 对象将这些操作封装为函数,例如 Reflect.deleteProperty(obj, prop)Reflect.has(obj, prop),使得这些操作可以在函数式编程中使用。

② 统一 Proxy 和 Object 的 API:Reflect 对象的方法与 Proxy 捕获器的方法一一对应。这样做的好处是,当你在 Proxy 捕获器中需要调用原始操作时,你可以直接使用 Reflect 对象的方法,而不需要记住不同操作的不同语法。

③ 更好的操作结果反馈:在 JavaScript 中,某些操作在失败时不会报错,而是静默失败,例如 delete obj.prop,如果 prop 是一个不可配置的属性,那么这个操作会失败,但是不会报错,只是返回 false。Reflect 对象的方法在操作失败时会抛出错误,使得错误更容易被捕获和处理。

④ 捕获器(set / get)的 receiver 参数可以决定对象访问器 getter / setter 中的 this。

javascript
let obj = {
  _value: 1,
  get value() {
    return this._value;
  },
  set value(v) {
    this._value = v;
  }
};

let p = new Proxy(obj, {
  get: function(target, prop, receiver) {
    console.log(`Getting ${prop}`);
    return Reflect.get(target, prop, receiver);
  },
  set: function(target, prop, value, receiver) {
    console.log(`Setting ${prop} to ${value}`);
    return Reflect.set(target, prop, value, receiver);
  }
});

p.value = 2;
console.log(p.value);

/*
  Setting value to 2
  Setting _value to 2
  Getting value
  Getting _value
  2
*/

常用方法

  1. Reflect.get(target, name):返回目标对象的属性值。
  2. Reflect.set(target, name, value):设置目标对象的属性值。
  3. Reflect.deleteProperty(target, name):删除目标对象的属性,返回一个布尔值表示是否成功。
  4. Reflect.has(target, name):判断目标对象是否拥有某个属性。
  5. Reflect.apply(target, thisArg, args):调用一个函数,等同于 Function.prototype.apply.call(func, thisArg, args)
  6. Reflect.construct(target, args):类似于 new target(...args),用于创建一个新的由目标函数构造的实例。
  7. Reflect.defineProperty(target, name, desc):和 Object.defineProperty 类似,定义或修改目标对象的属性,返回一个布尔值表示是否成功。
  8. Reflect.ownKeys(target):返回目标对象的所有自有属性的键名。
  9. Reflect.isExtensible(target):判断目标对象是否可扩展。
  10. Reflect.preventExtensions(target):阻止目标对象扩展,返回一个布尔值表示是否成功。
  11. Reflect.getOwnPropertyDescriptor(target, name):和 Object.getOwnPropertyDescriptor 类似,返回目标对象某个属性的描述符。
  12. Reflect.getPrototypeOf(target):和 Object.getPrototypeOf 类似,返回目标对象的原型。
  13. Reflect.setPrototypeOf(target, prototype):和 Object.setPrototypeOf 类似,设置目标对象的原型,返回一个布尔值表示是否成功。

8. 迭代器

1)是什么

迭代器是一种特殊的对象,它提供了一个 next 方法。这个方法返回一个包含两个属性(value 和 done)的对象。value 属性表示当前的值,done 属性是一个布尔值,表示是否还有更多的值可以迭代。

javascript
const names = ["abc", "cba", "nba"]
const nums = [100, 24, 55, 66, 86]

// 封装一个函数
function createArrayIterator(arr) {
  let index = 0
  return {
	next: function() {
	  if (index < arr.length) {
		return { done: false, value: arr[index++] }
	  } else {
		return { done: true }
	  }
	}
  }
}

const namesIterator = createArrayIterator(names)
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())

const numsIterator = createArrayIterator(nums)
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
2)可迭代对象

是什么

可迭代对象必须实现 @@iterator 方法。这个方法是使用 Symbol.iterator 作为键来定义的。

当调用 @@iterator 方法时,它应该返回一个迭代器对象。

迭代器对象必须实现 next() 方法。

next() 方法应该返回一个包含 value 和 done 属性的对象。

javascript
const infos = {
  name: "why",
  age: 18,
  height: 1.88,

  [Symbol.iterator]: function() {
	// const keys = Object.keys(this)
	// const values = Object.values(this)
	const entries = Object.entries(this)
	let index = 0
	const iterator = {
	  next: function() {
		if (index < entries.length) {
		  return { done: false, value: entries[index++] }
		} else {
		  return { done: true }
		}
	  }
	}
	return iterator
  }
}

for (const item of infos) {
  const [key, value] = item
  console.log(key, value)
}

通过上面的学习,我们就知道了,只要是可迭代对象,就会有 [Symbol.iterator] 属性。因此,原生可迭代对象也有这个属性。

javascript
const students = ["张三", "李四", "王五"]
console.log(students[Symbol.iterator])
const studentIterator = students[Symbol.iterator]()
console.log(studentIterator.next())
console.log(studentIterator.next())
console.log(studentIterator.next())
console.log(studentIterator.next())

可迭代对象的应用

原生可迭代对象:arguments、NodeList、String、Array、Set、Map ...

JavaScript 中语法:for ...of、展开语法、解构赋值、yield*(后面讲)

创建一些对象时:new Map([Iterable])、new WeakMap([Iterable])、new Set([Iterable])、new WeakSet([Iterable])

一些方法的调用:Promise.all([Iterable])、Promise.race([Iterable])、Array.from([Iterable])

迭代器的中断

送代器在某些情况下会在没有完全迭代的情况下中断:

  • 比如遍历的过程中通过 break、return、throw 中断了循环操作;
  • 比如在解构的时候,没有解构所有的值;

那么这个时候我们想要监听中断的话,可以添加 return 方法:

javascript
class Person {
  constructor(name, age, height, friends) {
	this.name = name
	this.age = age
	this.height = height
	this.friends = friends
  }

  // 实例方法
  running() {}

  [Symbol.iterator]() {
	let index = 0
	const iterator = {
	  next: () => {
		if (index < this.friends.length) {
		  return { done: false, value: this.friends[index++] }
		} else {
		  return { done: true }
		}
	  },
	  return: () => {
		console.log("监听到迭代器中断了")
		return { done: true }
	  }
	}
	return iterator
  }
}


const p1 = new Person("why", 18, 1.88, ["curry", "kobe", "james", "tatumu"])

for (const item of p1) {
  console.log(item)
  if (item === "kobe") {
	break
  }
}

9. 生成器

1)是什么

生成器(Generators)是一种特殊的函数,可以在执行过程中被暂停和恢复。生成器函数使用 function* 语法定义,并且使用 yield 关键字来暂停函数的执行。

当生成器函数被调用时,它返回一个遵循迭代器协议的生成器对象。你可以调用生成器对象的 next 方法来执行生成器函数的代码,直到遇到 yield 表达式。每次调用 next 方法,生成器函数都会从上次暂停的地方开始执行,直到遇到下一个 yield 表达式。

javascript
function* idGenerator() {
  let id = 0;
  while (true) {
    yield id++;
  }
}

let generator = idGenerator();

console.log(generator.next().value);  // 输出 0
console.log(generator.next().value);  // 输出 1
console.log(generator.next().value);  // 输出 2
2)如何使用

生成器函数参数返回值

javascript
function* gen() {
  let ask1 = yield "2 + 2 = ?";
  alert(ask1);  // 4
  let ask2 = yield "3 * 3 = ?"
  alert(ask2);  // 9
}

let generator = gen();
alert( generator.next().value );   // "2 + 2 = ?"
alert( generator.next(4).value );  // "3 * 3 = ?"
alert( generator.next(9).done );   // true

如果第一次调用 next() 方法需要传参,怎么办?在 let generator = gen(); 调用时传参。

生成器函数提前结束

javascript
function* foo(name1) {
  console.log("执行内部代码:1111", name1)
  console.log("执行内部代码:2222", name1)
  const name2 = yield "aaaa"
  console.log("执行内部代码:3333", name2)
  console.log("执行内部代码:4444", name2)
  const name3 = yield "bbbb"
  // return "bbbb"
  console.log("执行内部代码:5555", name3)
  console.log("执行内部代码:6666", name3)
  yield "cccc"

  console.log("最后一次执行")
  return undefined
}

const generator = foo("next1")

// 1.generator.return提前结束函数
console.log(generator.next())
console.log(generator.return("next2"))
console.log("-------------------")
console.log(generator.next("next3"))
console.log(generator.next("next4"))

// 2.generator.throw向函数抛出一个异常
console.log(generator.next())
console.log(generator.throw(new Error("next2 throw error")))
console.log("-------------------")
console.log(generator.next("next3"))
console.log(generator.next("next4"))
3)应用

生成器代替迭代器

javascript
// 1.对之前的代码进行重构 (用生成器函数)
const names = ["abc", "cba", "nba"]

function* createArrayIterator(arr) {
  for (let i = 0; i < arr.length; i++) {
	yield arr[i]
  }
  // yield arr[0]
  // yield arr[1]
  // yield arr[2]
  // return undefined
}

const namesIterator = createArrayIterator(names)
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())

// 2.生成器函数, 可以生成某个范围的值
// [3, 9)
function* createRangeGenerator(start, end) {
  for (let i = start; i < end; i++) {
	yield i
  }
}

const rangeGen = createRangeGenerator(3, 9)
console.log(rangeGen.next())
console.log(rangeGen.next())
console.log(rangeGen.next())
4)yield 语法糖

yield* 用于在一个生成器函数中调用另一个生成器函数或可迭代对象。当你在生成器函数中使用 yield*,JavaScript 会自动遍历指定的生成器或可迭代对象,并将其产生的所有值都 yield 出来。

在生成器函数中调用另一个生成器函数:

javascript
function* generatorA() {
  yield 1;
  yield 2;
}

function* generatorB() {
  yield* generatorA();
  yield 3;
}

let iterator = generatorB();

console.log(iterator.next().value);  // 输出 1
console.log(iterator.next().value);  // 输出 2
console.log(iterator.next().value);  // 输出 3

在生成器函数中遍历可迭代对象:

javascript
function* generator() {
  yield* [1, 2, 3];
}

let iterator = generator();

console.log(iterator.next().value);  // 输出 1
console.log(iterator.next().value);  // 输出 2
console.log(iterator.next().value);  // 输出 3

yield* 替换类中的实现:

javascript
class Person {
  constructor(name, age, height, friends) {
	this.name = name
	this.age = age
	this.height = height
	this.friends = friends
  }

  // 实例方法
  *[Symbol.iterator]() {
	yield* this.friends
  }
}

const p = new Person("why", 18, 1.88, ["kobe", "james", "curry"])
for (const item of p) {
  console.log(item)
}

const pIterator = p[Symbol.iterator]()
console.log(pIterator.next())
console.log(pIterator.next())
console.log(pIterator.next())
console.log(pIterator.next())

10. Class 的基本语法

[1] 基本认识

ES6 的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

ES5 中,生成实例对象的传统方法是通过构造函数。

javascript
function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);

ES6 之后。

javascript
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

类的所有方法都定义在类的 prototype 属性上面。

类的内部所有定义的方法,都是不可枚举的。这一点与 ES5 的行为不一致。

[2] 实例属性的新写法

ES2022 为类的实例属性,又规定了一种新写法。实例属性现在除了可以定义在constructor()方法里面的 this 上面,也可以定义在类内部的最顶层。

javascript
class IncreasingCounter {
  _count = 0;
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

实例属性 _count 与取值函数 value() 和 increment() 方法,处于同一个层级。这时,不需要在实例属性前面加上 this。

[3] getter 和 setter

与 ES5 一样,在“类”的内部可以使用 get 和 set 关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

存值函数和取值函数是设置在属性的 Descriptor 对象上的。

javascript
class CustomHTMLElement {
  constructor(element) {
    this.element = element;
  }

  get html() {
    return this.element.innerHTML;
  }

  set html(value) {
    this.element.innerHTML = value;
  }
}

var descriptor = Object.getOwnPropertyDescriptor(
  CustomHTMLElement.prototype, "html"
);

"get" in descriptor  // true
"set" in descriptor  // true

上面代码中,存值函数和取值函数是定义在 html 属性的描述对象上面,这与 ES5 完全一致。

[4] 静态方法 / 属性

静态方法

① 静态方法包含 this 关键字,这个 this 指的是类,而不是实例。

javascript
class Foo {
  static bar() {
    this.baz();
  }
  static baz() {
    console.log('hello');
  }
  baz() {
    console.log('world');
  }
}

Foo.bar() // hello

② 父类的静态方法,可以被子类继承。

③ 静态方法也是可以从 super 对象上调用的。

javascript
class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
  static classMethod() {
    return super.classMethod() + ', too';
  }
}

Bar.classMethod() // "hello, too"

静态属性

在实例属性的前面,加上 static 关键字。

javascript
class MyClass {
  static myStaticProp = 42;

  constructor() {
    console.log(MyClass.myStaticProp); // 42
  }
}
[5] 私有属性 / 方法

使用

在属性名之前使用 # 表示。

注意:

  • 私有属性不限于从 this 引用,只要是在类的内部,实例也可以引用私有属性。

    javascript
    class Foo {
      #privateValue = 42;
      static getPrivateValue(foo) {
        return foo.#privateValue;
      }
    }
    
    Foo.getPrivateValue(new Foo()); // 42
  • 私有属性和私有方法前面,也可以加上 static 关键字,表示这是一个静态的私有属性或私有方法。

检测

直接访问某个类不存在的私有属性会报错,但是访问不存在的公开属性不会报错。这个特性可以用来判断,某个对象是否为类的实例。

javascript
class C {
  #brand;

  static isC(obj) {
    try {
      obj.#brand;
      return true;
    } catch {
      return false;
    }
  }
}

这样的写法很麻烦,代码可读性很差,ES2022 改进了 in 运算符,使它也可以用来判断私有属性。

javascript
class C {
  #brand;

  static isC(obj) {
    if (#brand in obj) {
      // 私有属性 #brand 存在
      return true;
    } else {
      // 私有属性 #foo 不存在
      return false;
    }
  }
}

注意:

  • 也可以跟 this 一起配合使用。

    javascript
    class A {
      #foo = 0;
      m() {
        console.log(#foo in this); // true
      }
    }
  • 判断私有属性时,in 只能用在类的内部。

  • 判断所针对的私有属性,一定要先声明,否则会报错。

[6] 静态块

ES2022 引入了静态块(static block),允许在类的内部设置一个代码块,在类生成时运行且只运行一次,主要作用是对静态属性进行初始化。以后,新建类的实例时,这个块就不运行了。

javascript
class C {
  static x = ...;
  static y;
  static z;

  static {
    try {
      const obj = doSomethingWith(this.x);
      this.y = obj.y;
      this.z = obj.z;
    }
    catch {
      this.y = ...;
      this.z = ...;
    }
  }
}

静态块内部可以使用类名或 this,指代当前类。

三、ES7

之前学习过 Array 中的 includes 方法、指数运算符(**)。

四、ES8

1. Object.values

之前可以通过 Object.keys 获取一个对象所有的 key。

在 ES8 中提供了 Object.values 来获取所有的 value 值。

2. Object entries

可以针对对象、数组、字符串进行操作。返回结果是一个数组,数组中会存放可枚举属性的键值对数组。

javascript
let obj = { name: "张三", age: 30 };
console.log(Object.entries(obj));  // 输出 [["name", "张三"], ["age", 30]]

let arr = ["a", "b", "c"];
console.log(Object.entries(arr));  // 输出 [["0", "a"], ["1", "b"], ["2", "c"]]

let str = "abc";
console.log(Object.entries(str));  // 输出 [["0", "a"], ["1", "b"], ["2", "c"]]

3. String Padding

某些字符串需要对其进行前后的填充,来实现某种格式化效果。ES8 中增加 了 padStart 和 padEnd 方法,分别是对字符串的首尾进行填充的。

  • padStart(targetLength, padString) 方法会在字符串的开始添加填充字符,直到字符串达到 targetLength 的长度。如果字符串已经达到或超过 targetLength,则不会添加任何填充字符。

  • padEnd(targetLength, padString) 方法与 padStart 类似,但是它在字符串的结束添加填充字符。

    padString 参数是可选的,它是用于填充的字符串。如果没有提供 padString,则默认使用空格进行填充。

javascript
function maskCardNumber(cardNumber) {
  let lastFourDigits = cardNumber.slice(-4);
  return lastFourDigits.padStart(cardNumber.length, "*");
}

let cardNumber = "1234567890123456";
console.log(maskCardNumber(cardNumber)); // 输出 "************3456"

4. Trailing Commas

在 ES8 引入了尾随逗号(Trailing Commas)的特性。

这个特性允许在对象或数组、函数的最后一个元素后面添加一个逗号,而不会引发语法错误。

javascript
function foo(a, b, ){
  console.log(a, b)
}

foo(1, 2, )

5. 属性描述符

之前学习过 Object Descriptors。

6. async、await 关键字

后面学习 Async Function 中的 async、await 关键字。

五、ES9

Object spread operators(扩展运算符 / 展开运算符):前面讲过了。

Async iterators:后续迭代器讲解。

Promise finally:后续讲 Promise 讲解。

六、ES10

1. flat / flatMap

flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

javascript
let arr = [1, [2, 3], [4, [5, 6]]];

console.log(arr.flat());  // 输出 [1, 2, 3, 4, [5, 6]]
console.log(arr.flat(2));  // 输出 [1, 2, 3, 4, 5, 6]

flatMap() 方法首先对数组的每个元素执行一个函数(这个函数需要你提供),然后将结果平铺成一个新的一维数组。这个方法的效果等同于先对数组使用 map() 方法,然后对结果使用 flat() 方法,但 flatMap() 更高效。

flatMap 中的 flat 相当于深度为 1。

javascript
let arr = [1, 2, 3];
let result = arr.flatMap(x => [x, x * 2]);

console.log(result);  // 输出 [1, 2, 2, 4, 3, 6]

2. Object fromEntries

用于将一个键值对的列表转换为一个新的对象。

这个方法接受一个可迭代的参数,这个参数应该生成键值对的数组。每个键值对都应该是一个具有两个元素的数组,其中第一个元素是键,第二个元素是值。

javascript
let entries = [["name", "张三"], ["age", 30]];
let obj = Object.fromEntries(entries);

console.log(obj);  // 输出 { name: "张三", age: 30 }
javascript
let paramsString = "q=URLUtils.searchParams&topic=api";
let searchParams = new URLSearchParams(paramsString);

let obj = Object.fromEntries(searchParams);

console.log(obj);  // 输出 { q: "URLUtils.searchParams", topic: "api" }

3. trimStart / trimEnd

去除字符串首尾空格。

4. 其他

已经学习过 Symbol description。

后面学习 Optional catch binding(在不需要错误对象的情况下省略 catch 子句的参数)。

javascript
try {
  // 一些可能抛出错误的代码
} catch {
  // 处理错误,不需要使用 error 对象
}

七、ES11

1. BigInt

引入了新的数据类型 BigInt,用于表示大的整数。

BitInt 的表示方法是在数值的后面加上 n。

2. Nullish Coalescing Operator

空值合并运算符是 ES2020(ES11)中引入的一个新特性。它使用 ?? 来提供一个当左侧表达式结果为 null 或 undefined 时使用的默认值。

javascript
// 使用 ??
console.log(false ?? "默认值");      // 输出: false
console.log(0 ?? "默认值");          // 输出: 0
console.log('' ?? "默认值");         // 输出: ''
console.log(null ?? "默认值");       // 输出: "默认值"
console.log(undefined ?? "默认值");  // 输出: "默认值"

// 使用 ||
console.log(false || "默认值");      // 输出: "默认值"
console.log(0 || "默认值");          // 输出: "默认值"
console.log('' || "默认值");         // 输出: "默认值"
console.log(null || "默认值");       // 输出: "默认值"
console.log(undefined || "默认值");  // 输出: "默认值"

3. Optional Chaining

可选链允许在查询一个对象的深层属性时,不需要检查每一层是否存在。

可选链使用 ?. 运算符。如果左侧的值是 null 或 undefined,它将立即返回 undefined,而不会尝试访问更深层的属性。

链判断运算符 ?. 有三种写法。

  • obj?.prop // 对象属性是否存在。
  • obj?.[expr] // 同上。
  • func?.(...args) // 函数或对象方法是否存在。
javascript
let user = {
  name: "张三",
  address: {
    city: "北京"
  },
  method: function(x) {
    return x * 2;
  }
};

console.log(user.address.city);  // 输出 "北京"
console.log(user.profile.age);   // 抛出错误,因为 user.profile 是 undefined
console.log(user.profile?.age);  // 输出 undefined,不会抛出错误
let result1 = user.method?.(5);  // 调用 obj.method(5),结果是 10

4. for...in 标准化

在 ES11 之前,虽然很多浏览器支持 for...in 来遍历对象类型,但是并没有被 ECMA 标准化。

在 ES11 中,对其进行了标准化,for...in 是用于遍历对象的 key 的。

5. 其他

已经学习了 Global This(globalThis 提供了一个统一的方式来访问全局对象)。

Dynamic Import:后续 ES Module 模块化中讲解。

Promise.allSettled:后续讲 Promise 的时候讲解。

import meta:后续 ES Module 模块化中讲解。

八、ES12

1. FinalizationRegistry

一种注册清理回调的机制,这些回调会在注册的对象被垃圾回收时执行。

javascript
let registry = new FinalizationRegistry(heldValue => {
  // 清理代码
  console.log(heldValue); // some resource
});

let obj = {};

let timerId = setTimeout(() => {
  // 使用完后清理
  obj = null;        // 清除对象引用
  timerId = null;    // 清除定时器引用
}, 1000);

registry.register(obj, "some resource");

2. WeakRefs

一种引用一个对象,而不阻止该对象被垃圾回收的方式。

weakRef.deref() 是 WeakRef 的方法,用于获取其引用的对象。如果对象尚未被垃圾回收,则返回该对象;如果对象已经被回收,则返回 undefined。

javascript
let obj = {
  data: "some data"
};

let weakRef = new WeakRef(obj);

console.log(weakRef.deref().data);  // 输出 "some data"

3. 逻辑赋值运算符

这些运算符结合了逻辑运算符(如 &&||??)和赋值运算符(=)。

这是三个新的逻辑赋值运算符:

  • &&=:如果左侧的值为真,则将右侧的值赋给左侧。
  • ||=:如果左侧的值为假,则将右侧的值赋给左侧。
  • ??=:如果左侧的值为 null 或 undefined,则将右侧的值赋给左侧。
javascript
let a = 1;
let b = 0;
let c = null;

a &&= 2;  // a 现在是 2,因为 a 的原始值是真
b ||= 2;  // b 现在是 2,因为 b 的原始值是假
c ??= 2;  // c 现在是 2,因为 c 的原始值是 null

console.log(a);  // 输出 2
console.log(b);  // 输出 2
console.log(c);  // 输出 2

4. 其他

以前学习过 Numeric Separator、String.replaceAll。

九、ES13

1. method.at()

javascript
let arr = [1, 2, 3, 4, 5];
console.log(arr.at(-1));    // 输出 5

let str = "Hello, world!";
console.log(str.at(-1));    // 输出 "!"

2. Object.hasOwn(obj, propKey)

用于检查一个对象是否拥有特定的自身属性(不包括原型链上的属性)。

javascript
let obj = {
  prop: "value"
};

console.log(Object.hasOwn(obj, "prop"));      // 输出 true
console.log(Object.hasOwn(obj, "toString"));  // 输出 false

那么和之前学习的 Object.prototype.hasOwnProperty 有什么区别呢?

区别一:防止对象内部有重写 hasOwnProperty。

区别二:对于隐式原型指向 null 的对象, hasOwnProperty 无法进行判断。

javascript
let obj = Object.create(null);    // 创建一个没有原型链的对象
console.log(obj.hasOwnProperty);  // 输出 undefined

3. New members of classes

在 ES13 中,新增了定义 class 类中成员字段(field)的其他方式。

  • Instance public fields - 实例公有字段
  • Static public fields - 静态公有字段
  • Instance private fields - 实例私有字段
  • static private fields - 静态私有字段
  • static block - 静态代码块
javascript
class MyClass {
  // 实例公共字段
  instancePublicField = "instance public field";

  // 静态公共字段
  static staticPublicField = "static public field";

  // 实例私有字段
  #instancePrivateField = "instance private field";

  // 静态私有字段
  static #staticPrivateField = "static private field";

  // 静态块
  static {
    console.log(MyClass.#staticPrivateField);
  }
}

let myInstance = new MyClass();

console.log(myInstance.instancePublicField);  // 输出 "instance public field"
console.log(MyClass.staticPublicField);       // 输出 "static public field"

附加

一、浏览器的渲染原理

1. 浏览器内核

浏览器的内核主要分为两部分:渲染引擎和 JavaScript 引擎。渲染引擎负责将网页内容(HTML、CSS、图片等)渲染到屏幕上;JavaScript 引擎则负责解析和执行 JavaScript 代码。

浏览器内核主要可以分为以下几种:

  • Trident(三叉戟):也被称为 IE 内核,由微软开发,主要用于 Internet Explorer 浏览器。
  • Gecko(壁虎):由 Mozilla Foundation 开发,主要用于 Firefox 浏览器。
  • WebKit:最初由 Apple 开发,用于 Safari 浏览器。WebKit 是开源的,因此被许多浏览器采用,包括早期的 Chrome 和 Opera。
  • Blink:由 Google 开发,是 WebKit 的一个分支,用于 Chrome 浏览器和最新的 Microsoft Edge 浏览器。Opera 浏览器从 2013 年开始也转为使用 Blink 内核。
  • Presto(急板乐曲):由 Opera Software 开发,用于早期的 Opera 浏览器。从 2013 年开始,Opera 浏览器转为使用 Blink 内核。
  • EdgeHTML:由微软开发,用于早期的 Microsoft Edge 浏览器。从 2019 年开始,Microsoft Edge 浏览器转为使用 Blink 内核。

2. 网页被解析的过程

渲染页面的简化流程

渲染页面的详细流程

此篇文章摘自 浏览器的工作方式

获取 HTML:首先,浏览器会向服务器发送请求,获取 HTML 文件。

解析 HTML:浏览器会解析 HTML 文件,将其转换为 DOM(文档对象模型)树。DOM 树是 HTML 元素的层次结构表示。

获取并解析 CSS 和 JavaScript:在解析 HTML 的同时,浏览器会并行下载 CSS 文件和 JavaScript 文件。CSS 文件被解析为 CSSOM(CSS 对象模型)树,JavaScript 文件则被 JavaScript 引擎解析并执行。

构建渲染树:浏览器将 DOM 树和 CSSOM 树合并为一个渲染树。渲染树只包含需要显示在屏幕上的元素。

注意一:link 元素不会阻塞 DOM Tree 的构建过程 ,但是会阻塞 Render Tree 的构建过程。这是因为 Render Tree 在构建时,需要对应的 CSSOM Tree。

注意二:Render Tree 和 DOM Tree 并不是一一对应的关系,比如对于 display 为 none 的元素,压根不会出现在 render tree。

布局:一旦有了渲染树,浏览器就可以计算出每个元素在屏幕上的位置。这个过程被称为布局或重排。

绘制:最后,浏览器会遍历渲染树,使用 UI 后端层将每个节点绘制到屏幕上。这个过程被称为绘制或重绘。

这个过程可能会多次重复,因为 JavaScript 可能会修改 DOM 或 CSS,这可能会导致渲染树、布局和绘制的更新。这个过程被称为回流和重绘。

这个过程可能因浏览器的实现和优化策略而有所不同。例如,一些现代浏览器会尽早开始布局和绘制,即使 DOM 和 CSSOM 还没有完全解析。

3. 回流和重绘

回流(reflow):(也可以称之为重排)

第一次确定节点的大小和位置,称之为布局(layout)。之后对节点的大小、位置修改重新计算称之为回流。

什么情况下引起回流呢?

  • 比如 DOM 结构发生改变(添加新的节点或者移除节点)。

  • 比如改变了布局(修改了 width、height、padding、font-size 等值)。

  • 比如窗口 resize(修改了窗口的尺寸等)。

  • 比如调用 getComputedStyle 方法获取尺寸、位置信息。即使 getComputedStyle 只是获取样式信息,但为了确保获取的信息是最新的,浏览器可能需要先进行回流或重绘。

重绘(repaint)

第一次渲染内容称之为绘制(paint)。之后重新渲染称之为重绘。

什么情况下会引起重绘呢?

比如修改背景色、文字颜色、边框颜色、样式等;

回流一定会引起重绘,所以回流是一件很消耗性能的事情。因此,在开发中要尽量避免发生回流:

  • 修改样式时尽量一次性修改

    比如通过 cssText 修改,比如通过添加 class 修改。

  • 尽量避免频繁的操作 DOM

    可以在一个 DocumentFragment 或者父元素中将要操作的 DOM 操作完成后,再一次性执行添加操作。

  • 尽量避免通过 getComputedStyle 获取尺寸、位置等信息。

  • 对某些元素使用 position 的 absolute 或者 fixed。

    并不是不会引起回流,而是开销相对较小,不会对其他元素造成影响。

4. composite 合成

绘制的过程,可以将布局后的元素绘制到多个合成图层中。这是浏览器的一种优化手段。

默认情况下,标准流中的内容都是被绘制在同一个图层(Layer)中的。而一些特殊的属性,会创建一个新的合成层(CompositingLayer),并且新的图层可以利用 GPU 来加速绘制,因为每个合成层都是单独渲染的。

那么,哪些属性可以形成新的合成层呢?常见的一些属性:

  • 3D transforms
  • video、canvas、iframe
  • opacity 动画转换时
  • position: fixed
  • will-change:一个实验性的属性,提前告诉浏览器元素可能发生哪些变化。
  • animation 或 transition 设置了 opacity、transform

分层确实可以提高性能,但是它以内存管理为代价,因此不应作为 web 性能优化策略的一部分过度使用。

5. script 元素和页面解析的关系

现在已经知道了页面的渲染过程,但是 JavaScript 在哪里呢?

事实上,浏览器在解析 HTML 的过程中,遇到了 script 元素是不能继续构建 DOM 树的。它会停止继续构建,首先下载 JavaScript 代码,并且执行 JavaScript 的脚本。只有等到 JavaScript 脚本执行结束后,才会继续解析 HTML,构建 DOM 树。

为什么要这样做呢?

这是因为 JavaScript 的作用之一就是操作 DOM,并且可以修改 DOM。

如果我们等到 DOM 树构建完成并且渲染再执行 JavaScript,会造成严重的回流和重绘,影响页面的性能。所以会在遇到 script 元素时,优先下载和执行 JavaScript 代码,再继续构建 DOM 树。

但是这个也往往会带来新的问题,特别是现代页面开发中:

  • 在目前的开发模式中(比如 Vue、React),脚本往往比 HTML 页面更“重”,处理时间需要更长。
  • 所以会造成页面的解析阻塞,在脚本下载、执行完成之前,用户在界面上什么都看不到。

为了解决这个问题,script 元素给我们提供了两个属性:defer 和 async。

defer 属性

defer 属性告诉浏览器不要等待脚本下载 ,而继续解析 HTML,构建 DOM Tree。脚本会由浏览器来进行下载,但是不会阻塞 DOM Tree 的构建过程。

如果脚本提前下载好了,它会等待 DOM Tree 构建完成,在 DOMContentLoaded 事件之前先执行 defer 中的代码。所以,DOMContentLoaded 总是会等待 defer 中的代码先执行完成。另外多个带 defer 的脚本是可以保持正确的顺序执行的。

从某种角度来说,defer 可以提高页面的性能,并且推荐放到 head 元素中。

注意:defer 仅适用于外部脚本,对于 script 默认内容会被忽略。

async 属性

async 特性与 defer 有些类似,它也能够让脚本不阻塞页面。

async 是让一个脚本完全独立的:

  • 浏览器不会因 async 脚本而阻塞(与 defer 类似)。
  • async 脚本不能保证顺序,它是独立下载、独立运行,不会等待其他脚本。
  • async 不会能保证在 DOMContentLoaded 之前或者之后执行。

defer 通常用于需要在文档解析后操作 DOM 的 JavaScript 代码,并且对多个 script 文件有顺序要求的。

async 通常用于独立的脚本,对其他脚本,对 DOM 没有依赖的。

二、V8 引擎的执行原理

V8 是用 C ++ 编写的 Google 开源高性能 JavaScript 和 WebAssembly 引擎,它用于 Chrome 和 Node.js 等。

它实现 ECMAScript 和 WebAssembly。并在 Windows 7 或更高版本、macOS 10.12+、使用 x64, IA-32, ARM 或 MIPS 处理器的 Linux 系统上运行。

V8 可以独立运行,也可以嵌入到任何 C++ 应用程序中。

  • Parse:模块会将 JavaScript 代码转换成 AST(抽象语法树),这是因为解释器并不直接认识 JavaScript 代码。

    如果函数没有被调用,那么是不会被转换成 AST 的。

  • Ignition:一个解释器,会将 AST 转换成 ByteCode(字节码)。

    同时会收集 TurboFan 优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算)。

    如果函数只调用一次,Ignition 会解释执行 ByteCode。

  • TurboFan:是一个编译器,可以将字节码编译为 CPU 可以直接执行的机器码。

    如果一个函数被多次调用,就会被标记为热点函数,那么就会经过 TurboFan 转换成优化的机器码提高代码的执行性能。

    但是,机器码实际上也会被还原为 ByteCode,这是因为如果后续执行函数的过程中类型发生了变化(比如 sum 函数原来执行的是 number 类型,后来执行变成了 string 类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码。

  • 词法分析(lexical analysis):将字符序列转换成 token 序列的过程。

    token 是记号化(tokenization)的缩写。

    词法分析器(lexical analyzer,简称 lexer),也叫扫描器 scanner。

  • 语法分析(syntactic analysis,也叫 parsing)

    语法分析器也可以称之为解析器 parser。

V8 引擎的执行过程大致如下:

解析:首先,JavaScript 源代码被解析为抽象语法树(AST)。

解释:然后,V8 的解释器“快速解释器 (Ignition) ”将 AST 转换为字节码。

即时编译:在执行字节码的过程中,V8 会收集代码的运行信息。如果发现某些代码片段被频繁执行,那么这些代码片段会被送到即时编译器“优化编译器 (TurboFan) ”那里。TurboFan 会将这些字节码优化为高效的机器代码。

执行:最后,机器代码被直接执行。

在这个过程中,V8 使用了一种称为即时编译(JIT)的技术,可以在运行时将字节码优化为机器代码,以提高执行效率。同时,如果优化的代码片段不再频繁执行,V8 也可以将其反优化为字节码,以节省内存。

三、防抖

1. 认识防抖 debounce 函数

当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;

当事件密集触发时,函数的触发会被频繁的推迟;

只有等待了一段时间也没有事件触发,才会真正的执行响应函数;

2. 解决抖动

1)Underscore 库

第一步:引入 js 文件。

javascript
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.4/underscore-umd-min.js"></script>

第二步:调用 debounce 方法。

javascript
<input type="text">

……
.......

const inputEl = document.querySelector("input")
// 防抖处理代码
let counter = 1
inputEl.oninput = _.debounce(function() {
  console.log(`发送网络请求${counter++}:`, this.value)
}, 3000)
2)手写防抖函数

基本实现

javascript
function debounce(fn, delay) {
  // 1. 用于记录上一次事件触发的timer
  let timer = null

  // 2. 触发事件时执行的函数
  const _debounce = () => {
	// 2.1 如果有再次触发(更多次触发)事件, 那么取消上一次的事件
	if (timer) clearTimeout(timer)

	// 2.2 延迟去执行对应的fn函数(传入的回调函数)
	timer = setTimeout(() => {
	  fn()
	  timer = null // 执行过函数之后, 将timer重新置null
	}, delay);
  }

  // 返回一个新的函数
  return _debounce
}

this 和参数绑定

javascript
function debounce(fn, delay) {
  // 1. 用于记录上一次事件触发的timer
  let timer = null

  // 2. 触发事件时执行的函数
  const _debounce = function(...args) {
	// 2.1 如果有再次触发(更多次触发)事件, 那么取消上一次的事件
	if (timer) clearTimeout(timer)

	// 2.2 延迟去执行对应的fn函数(传入的回调函数)
	timer = setTimeout(() => {
	  fn.apply(this, args)
	  timer = null // 执行过函数之后, 将timer重新置null
	}, delay);
  }

  // 返回一个新的函数
  return _debounce
}

……
.......

let counter = 1
inputEl.oninput = debounce(function(event) {
  console.log(`发送网络请求${counter++}:`, this, event)
}, 1000)

取消功能实现

javascript
function debounce(fn, delay) {
  // 1. 用于记录上一次事件触发的timer
  let timer = null

  // 2. 触发事件时执行的函数
  const _debounce = function(...args) {
	// 2.1 如果有再次触发(更多次触发)事件, 那么取消上一次的事件
	if (timer) clearTimeout(timer)

	// 2.2 延迟去执行对应的fn函数(传入的回调函数)
	timer = setTimeout(() => {
	  fn.apply(this, args)
	  timer = null // 执行过函数之后, 将timer重新置null
	}, delay);
  }

  // 3. 给_debounce绑定一个取消的函数
  _debounce.cancel = function() {
	if (timer) clearTimeout(timer)
	timer = null
  }

  // 返回一个新的函数
  return _debounce
}

……
.......

let counter = 1
const debounceFn = debounce(function(event) {
  console.log(`发送网络请求${counter++}:`, this, event)
}, 5000)

inputEl.oninput = debounceFn

// 调用取消的功能
cancelBtn.onclick = function() {
  debounceFn.cancel()
}

立即执行功能

javascript
function debounce(fn, delay, immediate = false) {
  // 1. 用于记录上一次事件触发的timer
  let timer = null
  let isInvoke = false

  // 2. 触发事件时执行的函数
  const _debounce = function(...args) {
	// 2.1 如果有再次触发(更多次触发)事件, 那么取消上一次的事件
	if (timer) clearTimeout(timer)

	// 第一次操作是不需要延迟
	if (immediate && !isInvoke) {
	  fn.apply(this, args)
	  isInvoke = true
	  return
	}

	// 2.2 延迟去执行对应的fn函数(传入的回调函数)
	timer = setTimeout(() => {
	  fn.apply(this, args)
	  timer = null // 执行过函数之后, 将timer重新置null
	  isInvoke = false
	}, delay);
  }

  // 3. 给_debounce绑定一个取消的函数
  _debounce.cancel = function() {
	if (timer) clearTimeout(timer)
	timer = null
	isInvoke = false
  }

  // 返回一个新的函数
  return _debounce
}

获取返回值

javascript
function debounce(fn, delay, immediate = false, resultCallback) {
  // 1. 用于记录上一次事件触发的timer
  let timer = null
  let isInvoke = false

  // 2. 触发事件时执行的函数
  const _debounce = function(...args) {
	return new Promise((resolve, reject) => {
	  try {
		// 2.1 如果有再次触发(更多次触发)事件, 那么取消上一次的事件
		if (timer) clearTimeout(timer)

		// 第一次操作是不需要延迟
		let res = undefined
		if (immediate && !isInvoke) {
		  res = fn.apply(this, args)
		  // if (resultCallback) resultCallback(res)
		  resolve(res)
		  isInvoke = true
		  return
		}

		// 2.2 延迟去执行对应的fn函数(传入的回调函数)
		timer = setTimeout(() => {
		  res = fn.apply(this, args)
		  // if (resultCallback) resultCallback(res)
		  resolve(res)
		  timer = null // 执行过函数之后, 将timer重新置null
		  isInvoke = false
		}, delay);
	  } catch (error) {
		reject(error)
	  }
	})
  }

  // 3. 给_debounce绑定一个取消的函数
  _debounce.cancel = function() {
	if (timer) clearTimeout(timer)
	timer = null
	isInvoke = false
  }

  // 返回一个新的函数
  return _debounce
}

四、节流

1. 什么是节流

当事件触发时,会执行这个事件的响应函数;

如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数;

不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的;

2. 实现节流

1)Underscore 库

第一步:引入 js 文件。

javascript
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.4/underscore-umd-min.js"></script>

第二步:调用 throttle 方法。

javascript
<input type="text">

……
.......

const inputEl = document.querySelector("input")
// 防抖处理代码
let counter = 1
inputEl.oninput = _.throttle(function() {
  console.log(`发送网络请求${counter++}:`, this.value)
}, 3000)
2)手写节流函数

基本实现

javascript
function throttle(fn, interval) {
  let startTime = 0

  const _throttle = function() {
	const nowTime = new Date().getTime()
	const waitTime = interval - (nowTime - startTime)
	if (waitTime <= 0) {
	  fn()
	  startTime = nowTime
	}
  }

  return _throttle
}

this 和参数绑定

javascript
function throttle(fn, interval) {
  let startTime = 0

  const _throttle = function(...args) {
	const nowTime = new Date().getTime()
	const waitTime = interval - (nowTime - startTime)
	if (waitTime <= 0) {
	  fn.apply(this, args)
	  startTime = nowTime
	}
  }

  return _throttle
}

立即执行控制

javascript
/*
  leading = true (默认):第一次调用时立即执行
  leading = false:第一次调用时不立即执行,需要等待间隔时间
*/
function throttle(fn, interval, leading = true) {
  let startTime = 0

  const _throttle = function(...args) {
	// 1.获取当前时间
	const nowTime = new Date().getTime()

	// 对立即执行进行控制
	if (!leading && startTime === 0) {
	  startTime = nowTime
	}

	// 2.计算需要等待的时间执行函数
	const waitTime = interval - (nowTime - startTime)
	if (waitTime <= 0) {
	  fn.apply(this, args)
	  startTime = nowTime
	}
  }

  return _throttle
}

尾部执行控制

javascript
function throttle(fn, interval, { leading = true, trailing = false } = {}) {
  let startTime = 0
  let timer = null

  const _throttle = function(...args) {
    // 1. 获取当前时间
	const nowTime = new Date().getTime()

	// 对立即执行进行控制
	if (!leading && startTime === 0) {
	  startTime = nowTime
	}

	// 2. 计算需要等待的时间执行函数
	const waitTime = interval - (nowTime - startTime)
	if (waitTime <= 0) {
	  if (timer) clearTimeout(timer)
	  fn.apply(this, args)
	  startTime = nowTime
      timer = null
	  return
	}

	// 3. 判断是否需要执行尾部
	if (trailing && !timer) {
	  timer = setTimeout(() => {
	    fn.apply(this, args)
		startTime = new Date().getTime()
		timer = null
	  }, waitTime);
	}
  }

  return _throttle
}

取消功能实现

javascript
function throttle(fn, interval, { leading = true, trailing = false } = {}) {
  let startTime = 0
  let timer = null

  const _throttle = function(...args) {
    // 1. 获取当前时间
	const nowTime = new Date().getTime()

	// 对立即执行进行控制
	if (!leading && startTime === 0) {
	  startTime = nowTime
	}

	// 2. 计算需要等待的时间执行函数
	const waitTime = interval - (nowTime - startTime)
	if (waitTime <= 0) {
	  if (timer) clearTimeout(timer)
	  fn.apply(this, args)
      startTime = nowTime
      timer = null
	  return
	}

	// 3. 判断是否需要执行尾部
    if (trailing && !timer) {
	  timer = setTimeout(() => {
		fn.apply(this, args)
		startTime = new Date().getTime()
		timer = null
	  }, waitTime);
	}
  }

  _throttle.cancel = function() {
    if (timer) clearTimeout(timer)
    startTime = 0
    timer = null
  }

  return _throttle
}

获取返回值

javascript
function hythrottle(fn, interval, { leading = true, trailing = false } = {}) {
  let startTime = 0
  let timer = null

  const _throttle = function(...args) {
    return new Promise((resolve, reject) => {
      try {
        // 1. 获取当前时间
		const nowTime = new Date().getTime()

		// 对立即执行进行控制
		if (!leading && startTime === 0) {
		  startTime = nowTime
		}

		// 2. 计算需要等待的时间执行函数
		const waitTime = interval - (nowTime - startTime)
		if (waitTime <= 0) {
		  // console.log("执行操作fn")
		  if (timer) clearTimeout(timer)
		  const res = fn.apply(this, args)
		  resolve(res)
		  startTime = nowTime
		  timer = null
		  return
		} 

		// 3. 判断是否需要执行尾部
		if (trailing && !timer) {
		  timer = setTimeout(() => {
			// console.log("执行timer")
			const res = fn.apply(this, args)
			resolve(res)
			startTime = new Date().getTime()
			timer = null
		  }, waitTime);
		}
	  } catch (error) {
		reject(error)
	  }
	})
  }

  _throttle.cancel = function() {
	if (timer) clearTimeout(timer)
	startTime = 0
	timer = null
  }

  return _throttle
}

五、深浅拷贝

1. 浅拷贝

1)Object.assign()

javascript
let obj = { a: 1, b: { c: 2 } };
let shallowCopy = Object.assign({}, obj);

2)展开运算符(同时也适用于数组)

javascript
let obj = { a: 1, b: { c: 2 } };
let shallowCopy = { ...obj };

3)Array.prototype.slice()

javascript
let arr = [1, 2, [3, 4]];
let shallowCopy = arr.slice();  // slice 只能用于数组

2. 深拷贝

1)JSON.parse(JSON.stringify())

javascript
let obj = { a: 1, b: { c: 2 } };
let deepCopy = JSON.parse(JSON.stringify(obj));

注意:这种方法有局限性,不能处理函数、undefined、Symbol 等。

2)使用第三方库,例如 lodash 的 _.cloneDeep() 方法

javascript
const _ = require('lodash');
let obj = { a: 1, b: { c: 2 } };
let deepCopy = _.cloneDeep(obj);

3)递归实现

① 封装一个工具函数,判断标识符是否是对象类型

javascript
/*
   typeof 函数结果解析:
     null -> object
     function -> function
     object/array -> object
*/

// 判断一个标识符是否是对象类型
function isObject(value) {
  const valueType = typeof value
  // 此函数只当是 object/array 和 function 时才返回 true
  return (value !== null) && ( valueType === "object" || valueType === "function" )
}

② 深拷贝实现

javascript
// map 用于解决循环引用问题
function deepCopy(originValue, map = new WeakMap()) {
  // 6. 如果值是Symbol的类型
  if (typeof originValue === "symbol") {
    return Symbol(originValue.description)
  }

  // 1. 如果是原始类型, 直接返回
  if (!isObject(originValue)) {
    return originValue
  }

  // 3. 如果是set类型
  if (originValue instanceof Set) {
    const newSet = new Set()
    for (const setItem of originValue) {
	  newSet.add(deepCopy(setItem))
    }
    return newSet
  }

  // 4. 如果是函数function类型, 不需要进行深拷贝
  if (typeof originValue === "function") {
    return originValue
  }

  // 2. 如果是对象类型, 才需要创建对象 (避免循环引用)
  if (map.get(originValue)) {
    return map.get(originValue)
  }
  const newObj = Array.isArray(originValue) ? []: {}
  map.set(originValue, newObj)
  // 遍历普通的key
  for (const key in originValue) {
    newObj[key] = deepCopy(originValue[key], map)
  }
  // 5. 单独遍历symbol
  const symbolKeys = Object.getOwnPropertySymbols(originValue)
  for (const symbolKey of symbolKeys) {
    newObj[Symbol(symbolKey.description)] = deepCopy(originValue[symbolKey], map)
  }

  return newObj
}

六、事件总线

EventBus 是 JavaScript 中一种常用的设计模式,主要用于实现组件或模块之间的解耦通信。

javascript
class EventBus {
  constructor() {
    this.eventMap = {}
  }

  on(eventName, eventFn) {
    let eventFns = this.eventMap[eventName]
    if (!eventFns) {
	  eventFns = []
	  this.eventMap[eventName] = eventFns
	}
	eventFns.push(eventFn)
  }

  off(eventName, eventFn) {
    let eventFns = this.eventMap[eventName]
    if (!eventFns) return
	for (let i = 0; i < eventFns.length; i++) {
	  const fn = eventFns[i]
      if (fn === eventFn) {
	    eventFns.splice(i, 1)
	    break
      }
    }

    // 如果eventFns已经清空了
    if (eventFns.length === 0) {
      delete this.eventMap[eventName]
    }
  }

  emit(eventName, ...args) {
    let eventFns = this.eventMap[eventName]
    if (!eventFns) return
    eventFns.forEach(fn => {
      fn(...args)
    })
  }
}

使用过程

javascript
const eventBus = new EventBus()

// aside.vue 组件中监听事件
eventBus.on("navclick", (name, age, height) => {
  console.log("navclick listener 01", name, age, height)
})

const click =  () => {
  console.log("navclick listener 02")
}
eventBus.on("navclick", click)

setTimeout(() => {
  eventBus.off("navclick", click)
}, 5000);

eventBus.on("asideclick", () => {
  console.log("asideclick listener")
})


// nav.vue 组件中监听事件
const navBtnEl = document.querySelector(".nav-btn")
navBtnEl.onclick = function() {
  console.log("自己监听到")
  eventBus.emit("navclick", "why", 18, 1.88)
}

Released under the MIT License.