0%

在 javascript 中,this 关键字是一个特殊的对象标识符,它指向当前执行上下文的对象。与其他编程语言不同,javascript 的 this 不是在编写时绑定,而是在运行时根据函数的调用方式动态绑定的。理解 this 的指向是掌握 javascript 的关键,也是许多开发者容易混淆的地方。

全局上下文中的 this

在全局执行环境(浏览器中是 window,Node.js 中是 global)中,this 指向全局对象。

1
2
3
4
5
// 浏览器环境
console.log(this === window); // true

var name = 'globalName';
console.log(this.name); // 'globalName'

函数调用中的 this

非严格模式

在普通函数调用中,this 指向全局对象。

1
2
3
4
function showThis() {
console.log(this);
}
showThis(); // 浏览器中输出 window

严格模式

如果函数处于严格模式(‘use strict’),this 会保持为 undefined,因为严格模式下禁止默认指向全局。

1
2
3
4
5
'use strict';
function showThisStrict() {
console.log(this);
}
showThisStrict(); // undefined

方法调用中的 this

当函数作为对象的方法被调用时,this 指向调用该方法的对象。

1
2
3
4
5
6
7
8
9
10
11
const person = {
name: 'Alice',
greet: function() {
console.log('Hello, ' + this.name);
}
};
person.greet(); // Hello, Alice

// 如果将方法赋值给变量再调用,this 会丢失
const greetFn = person.greet;
greetFn(); // Hello, undefined (非严格模式下 this 指向 window)

构造函数中的 this

使用 new 关键字调用函数时,会创建一个新对象,并将该对象绑定到函数的 this 上。构造函数通常首字母大写。

1
2
3
4
5
function Person(name) {
this.name = name;
}
const alice = new Person('Alice');
console.log(alice.name); // Alice

如果构造函数显式返回一个对象,则 this 会被替换;如果返回基本类型,则忽略,依然返回 this 指向的新对象。

箭头函数中的 this

箭头函数没有自己的 this,它捕获其所在(定义时)上下文的 this 值。箭头函数的 this 在定义时就确定了,之后不会改变。

1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
name: 'obj',
normalFunc: function() {
console.log(this.name);
},
arrowFunc: () => {
console.log(this.name);
}
};

obj.normalFunc(); // 'obj'
obj.arrowFunc(); // undefined(箭头函数的 this 继承自全局/外层,此处外层是全局,所以 this 指向 window)

箭头函数的 this 不能被 call、apply、bind 改变。

事件处理中的 this

在 DOM 事件处理函数中,this 通常指向绑定事件的元素。

1
2
3
4
5
6
7
<button id="btn">Click me</button>
<script>
const btn = document.getElementById('btn');
btn.addEventListener('click', function() {
console.log(this); // <button id="btn">Click me</button>
});
</script>

如果使用箭头函数,this 会继承外层上下文(比如 window),可能不是期望的元素。

改变 this 指向:call、apply、bind

我们可以使用 call、apply 或 bind 显式指定函数调用时的 this。

  1. call(thisArg, arg1, arg2, …):立即调用函数,并指定 this。
  2. apply(thisArg, [argsArray]):与 call 类似,但参数以数组形式传递。
  3. bind(thisArg, arg1, arg2, …):返回一个新函数,其 this 永久绑定到指定对象,不会立即执行。
1
2
3
4
5
6
7
8
9
10
11
function introduce(age, city) {
console.log(`I'm ${this.name}, ${age} years old, from ${city}.`);
}

const user = { name: 'Bob' };

introduce.call(user, 25, 'New York'); // I'm Bob, 25 years old, from New York.
introduce.apply(user, [30, 'London']); // I'm Bob, 30 years old, from London.

const boundIntroduce = introduce.bind(user, 28);
boundIntroduce('Paris'); // I'm Bob, 28 years old, from Paris.

常见陷阱与注意事项

  1. 回调函数中的 this 丢失:将对象方法作为回调传递时,常常丢失 this。
1
2
3
4
5
6
7
8
9
10
const counter = {
count: 0,
increment: function() {
console.log(this.count);
}
};
setTimeout(counter.increment, 1000); // 输出 undefined(this 指向全局)
// 解决方法:使用箭头函数、bind 或包装函数
setTimeout(() => counter.increment(), 1000); // 0
setTimeout(counter.increment.bind(counter), 1000); // 0
  1. 嵌套函数中的 this:在方法内部定义普通函数时,该函数的 this 会指向全局(非严格模式)或 undefined(严格模式)。
1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
name: 'obj',
outer: function() {
console.log('outer this:', this.name); // 'obj'
function inner() {
console.log('inner this:', this.name); // undefined(非严格模式 window.name)
}
inner();
}
};
obj.outer();
// 解决:使用箭头函数、保存 this 变量(const self = this)、bind

总结

  1. this 的指向取决于函数的调用方式,而不是定义位置。
  2. 全局函数调用 → 指向全局对象(严格模式下 undefined)。
  3. 对象方法调用 → 指向调用该方法的对象。
  4. 构造函数调用(new)→ 指向新创建的实例。
  5. 箭头函数 → 继承定义时外层上下文的 this。
  6. 事件处理 → 指向绑定事件的元素。
  7. 可以使用 call、apply、bind 显式绑定 this。

随着前端应用日益复杂,javascript 模块化成为组织代码的必备手段。目前最主流的两种模块规范是 CommonJS 和 ES6 模块(ES Module)。CommonJS 主要用于 Node.js 服务端,而 ES6 模块则是 ECMAScript 官方标准,支持浏览器和现代 Node.js。虽然二者目标相似,但在语法、加载机制、导出值等方面存在显著差异。

语法差异

CommonJS 语法

  1. 导出:使用 module.exports 或 exports 对象。
  2. 导入:使用 require() 函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
// CommonJS 导出 (a.js)
const name = 'CommonJS';
const greet = () => console.log('Hello');
module.exports = { name, greet };

// 或者
exports.name = 'CommonJS';
exports.greet = () => console.log('Hello');

// CommonJS 导入 (b.js)
const mod = require('./a.js');
console.log(mod.name); // 'CommonJS'
mod.greet(); // 'Hello'

ES6 模块语法

  1. 导出:使用 export 关键字(命名导出或默认导出)。
  2. 导入:使用 import 关键字。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ES6 导出 (a.js)
export const name = 'ES6 Module';
export function greet() {
console.log('Hello');
}
// 默认导出
export default function() {
console.log('Default export');
}

// ES6 导入 (b.js)
import defaultFn, { name, greet } from './a.js';
console.log(name); // 'ES6 Module'
greet(); // 'Hello'
defaultFn(); // 'Default export'

加载机制:静态 vs 动态

CommonJS 动态加载

  1. require 可以在代码任意位置调用,可以动态拼接路径、条件加载。
  2. 模块在运行时同步加载。
1
2
3
// 动态加载示例
const moduleName = './module-' + Math.random() + '.js';
const mod = require(moduleName); // 运行时确定路径

ES6 模块静态加载

  1. import 和 export 必须位于模块顶层,不能嵌套在条件块内。
  2. 模块依赖在编译时(静态分析)确定,有利于 tree shaking 和优化。
1
2
3
4
5
6
7
// 错误:import 不能在代码块中
if (condition) {
import mod from './mod.js'; // 语法错误
}

// 正确:静态导入
import mod from './mod.js';

虽然 ES6 也提供了动态导入 import() 函数(返回 Promise),但那是运行时的异步加载,属于例外。

导出值的绑定:值的拷贝 vs 值的引用

CommonJS:导出值的拷贝

CommonJS 模块导出的是值的浅拷贝,一旦模块执行完毕,导出值就固定了;模块内部的后续变化不会影响已导入的值(除非导出的是引用类型,修改引用内部属性会体现)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// counter.js
let count = 0;
module.exports = {
count,
increment() {
count++;
console.log('内部 count:', count);
}
};

// main.js
const { count, increment } = require('./counter.js');
console.log(count); // 0
increment(); // 内部 count: 1
console.log(count); // 0 (count 仍为 0,并未改变)

ES6 模块:动态只读绑定

ES6 模块导出的是值的动态只读引用,导入的变量与原始变量绑定,原始值变化时,导入值也会相应变化(但导入方不能修改导入的变量,除非它本身是对象)。

1
2
3
4
5
6
7
8
9
10
11
12
// counter.js
export let count = 0;
export function increment() {
count++;
console.log('内部 count:', count);
}

// main.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment(); // 内部 count: 1
console.log(count); // 1 (count 随内部变化)

this 指向

  1. CommonJS 中,模块顶层的 this 指向当前模块的 exports 对象。
  2. ES6 模块 中,模块顶层的 this 是 undefined。
1
2
3
4
5
// CommonJS 模块
console.log(this === module.exports); // true

// ES6 模块
console.log(this); // undefined

循环依赖处理

CommonJS 的循环依赖

CommonJS 遇到循环依赖时,会返回当前已执行的导出部分(未完成的导出可能是不完整的对象),容易导致问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// a.js
exports.done = false;
const b = require('./b.js');
console.log('a 中 b.done =', b.done);
exports.done = true;
console.log('a 执行完毕');

// b.js
exports.done = false;
const a = require('./a.js');
console.log('b 中 a.done =', a.done);
exports.done = true;
console.log('b 执行完毕');

// main.js
const a = require('./a.js');
// 输出顺序:
// b 中 a.done = false
// b 执行完毕
// a 中 b.done = true
// a 执行完毕

ES6 模块的循环依赖

由于 ES6 模块是动态绑定,导入的是值的引用,即使在循环依赖中也能获取到正确的值(但需要依赖顺序,且变量提升机制使函数声明等可提前访问)。

1
2
3
4
5
6
7
8
9
10
11
12
13
// a.mjs
import { b } from './b.mjs';
export const a = 'a';
console.log('a 模块执行');

// b.mjs
import { a } from './a.mjs';
export const b = 'b';
console.log('b 模块执行, a =', a);

// 运行 a.mjs,输出:
// b 模块执行, a = undefined (因为 a 尚未初始化)
// a 模块执行

ES6 模块的循环依赖通常比 CommonJS 更可预测,但若导入未初始化的变量会得到 undefined,需注意编码顺序。

同步 vs 异步

  1. CommonJS 模块加载是同步的,因为 Node.js 早期服务端读取本地文件很快,且无需考虑网络。
  2. ES6 模块 设计为异步加载,适合浏览器环境,可以与 <script type="module"> 配合,支持按需加载。静态 import 是异步的,但语法上看起来是静态声明;动态 import() 返回 Promise。

静态分析与优化

  1. ES6 模块 支持静态分析,可以在编译阶段确定导入导出关系,从而实现 tree shaking(摇树优化)、死代码消除等。
  2. CommonJS 的模块结构是动态的,难以静态分析,通常打包工具需要额外处理才能实现优化。

使用环境

  1. CommonJS 主要运行在 Node.js 环境(通过 require),浏览器中需要打包工具(如 Browserify、Webpack)才能使用。
  2. ES6 模块 现代浏览器原生支持 <script type="module">,Node.js 从 12.x 开始逐步稳定支持(需将文件后缀改为 .mjs 或 package.json 中设置 “type”: “module”)。目前二者在 Node.js 中可以共存,但互操作有一些限制。

总结

特性 CommonJS ES6 模块
语法 require / module.exports import / export
加载时机 运行时同步 编译时静态(静态导入)+ 运行时异步(动态导入)
导出绑定 值的拷贝(基本类型) 值的动态只读引用
顶层this exports 对象 undefined
循环依赖 返回已执行部分的拷贝 动态绑定,但可能遇到暂时性死区
静态分析 不支持 支持,可实现 tree shaking
适用环境 Node.js(原生)、浏览器(打包后) 现代浏览器、Node.js(需配置)

javascript 是一门单线程语言,为了协调同步任务与异步任务,它引入了事件循环(Event Loop)的机制。然而,javascript 的运行环境主要有两种——浏览器和 Node.js,它们的事件循环实现方式有显著差异。理解这些差异对于编写可靠的跨环境代码至关重要。

宏观架构差异

浏览器事件循环

浏览器的事件循环由宿主环境(浏览器)实现,主要协调以下组成部分:

  • 调用栈(Call Stack):执行同步代码。
  • 宏任务队列(MacroTask Queue):存放 setTimeout、setInterval、I/O、UI 渲染等回调 。
  • 微任务队列(MicroTask Queue):存放 Promise.then、MutationObserver 等回调 。
  • 渲染引擎:负责页面绘制,与事件循环协同工作 。

Node.js 事件循环

Node.js 的事件循环由 libuv 库 实现,它包含多个阶段(phase),每个阶段都有自己的任务队列 。Node.js 的事件循环结构更复杂,因为它需要处理底层操作系统的 I/O 操作。

维度 浏览器事件循环 Node.js 事件循环
核心实现 浏览器宿主环境 libuv 库
阶段划分 简化为宏任务队列 + 微任务队列 细分为 6 个阶段(timers、I/O、poll、check 等)
任务粒度 每个宏任务队列执行一个任务就切换 每个阶段会清空当前阶段的整个队列
微任务执行时机 每个宏任务执行后清空微任务 每个阶段之后清空微任务(Node 11+ 与浏览器对齐)

Node.js 事件循环的六个阶段

Node.js 的事件循环按照以下顺序循环执行 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   ┌───────────────────────────┐
┌─>│ timers │ 执行 setTimeout、setInterval 的回调
│ └───────────┬───────────────┘
│ ┌───────────▼───────────────┐
│ │ I/O callbacks │ 执行延迟到下一轮的 I/O 回调
│ └───────────┬───────────────┘
│ ┌───────────▼───────────────┐
│ │ idle, prepare │ 系统内部使用
│ └───────────┬───────────────┘
│ ┌───────────▼───────────────┐
│ │ poll │ 检索新的 I/O 事件,执行 I/O 回调
│ └───────────┬───────────────┘
│ ┌───────────▼───────────────┐
│ │ check │ 执行 setImmediate 回调
│ └───────────┬───────────────┘
│ ┌───────────▼───────────────┐
│ │ close callbacks │ 执行 socket 等关闭事件的回调
│ └───────────────────────────┘
└──────────────┘

每个阶段结束后,Node.js 会先清空微任务队列(process.nextTick 和 Promise.then),然后进入下一阶段 。

核心差异详解

任务队列结构

  • 浏览器:只有两个任务队列(宏任务、微任务)。
  • Node.js:每个阶段都有独立的队列,外加两个微任务队列(nextTick 队列和 Promise 队列)。

任务执行粒度

  • 浏览器:从宏任务队列中取出第一个任务执行,然后清空微任务队列,接着检查是否需要渲染,再取下一个宏任务 。
  • Node.js:进入一个阶段后,会清空该阶段队列中的所有任务,然后才执行微任务,再进入下一阶段 。

微任务执行时机

  • 浏览器:每个宏任务执行完毕后清空微任务 。
  • Node.js:
    – Node 11 之前:在阶段切换时才清空微任务,可能导致一个阶段内积累大量微任务 。
    – Node 11 及之后:在每个宏任务执行后也会清空微任务,行为与浏览器基本对齐 。

特殊 API

  • 浏览器独有:requestAnimationFrame、MutationObserver 。
  • Node.js 独有:setImmediate、process.nextTick 。
    – setImmediate 在 check 阶段执行 。
    – process.nextTick 不属于事件循环的任何阶段,而是在每个阶段结束后立即执行,优先级高于 Promise 。

代码示例验证差异

示例 1:setTimeout 与 setImmediate 的执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- Node.js 环境 -->
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));

--> 执行结果可能不确定 :
--> 1. 如果事件循环进入 timers 阶段时,定时器时间已到 → setTimeout 先执行。
--> 2. 如果准备时间较短,定时器还没到期 → 跳过 timers,进入 poll → check → setImmediate 先执行。

<!-- 固定顺序的技巧:将代码放在 I/O 回调内 -->
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate')); // 总是先于 setTimeout
}); // 输出:setImmediate → setTimeout

--> 因为在 I/O 回调中,事件循环当前处于 poll 阶段,下一阶段是 check,所以 setImmediate 总是先执行 。

示例 2:process.nextTick 与 Promise 的优先级

1
2
3
4
5
6
7
8
9
10
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
setTimeout(() => console.log('setTimeout'), 0);

<!-- Node.js 输出顺序: -->
nextTick
promise
setTimeout

--> process.nextTick 优先级高于 Promise,且在所有阶段切换时都会执行 。

示例 3:Node 11 前后的行为差异

1
2
3
4
5
6
7
8
9
10
11
12
13
setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => console.log('promise1'));
}, 0);

setTimeout(() => {
console.log('timeout2');
Promise.resolve().then(() => console.log('promise2'));
}, 0);

--> Node 10:timeout1 → timeout2 → promise1 → promise2(每个阶段内先清空所有宏任务,再执行微任务)
--> Node 11+ 及浏览器:timeout1 → promise1 → timeout2 → promise2(每个宏任务后执行微任务)

总结与建议

对比维度 浏览器事件循环 Node.js 事件循环
核心模型 宏任务 + 微任务 多阶段 + 微任务
微任务时机 每个宏任务后 每个阶段后(Node 11+ 也支持每个宏任务后)
特殊 API requestAnimationFrame setImmediate, process.nextTick
复杂程度 相对简单 更复杂,涉及系统 I/O

编写跨环境代码的建议

  1. 避免依赖执行顺序:尽量不要编写依赖 setTimeout 与 setImmediate 执行顺序的业务代码。
  2. 使用 queueMicrotask:如果需要微任务,优先使用标准 API queueMicrotask 而不是 process.nextTick。
  3. 了解版本差异:Node 11+ 的事件循环与浏览器行为更接近,但仍需注意 nextTick 等特有 API 的存在 。
  4. 利用 I/O 回调稳定顺序:在 Node.js 中,如果需要在 I/O 之后立即执行任务,setImmediate 是最可靠的选择 。

在 javascript 中,拷贝对象或数组是常见的操作。然而,由于引用类型的存在,拷贝分为浅拷贝和深拷贝两种方式。理解它们的区别对于避免意外的数据共享和修改至关重要。

基本概念

在 javascript 中,数据类型分为基本类型(如 string、number、boolean、null、undefined、symbol、bigint)和引用类型(如 Object、Array、Function、Date、RegExp 等)。

  1. 基本类型:存储的是实际值,拷贝时直接复制值,互不影响。
  2. 引用类型:存储的是内存地址,拷贝时复制的是地址引用,多个变量可能指向同一个对象。

浅拷贝

创建一个新对象,但这个新对象会复制原对象的所有属性值。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址,因此新旧对象会共享同一块内存数据,修改其中一个会影响另一个。

深拷贝

创建一个全新的对象,递归地复制原对象的所有属性。新对象和原对象完全隔离,修改互不影响。

浅拷贝的实现方式

  1. 手动赋值(只适用于简单对象)
1
2
3
4
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: obj1.a, b: obj1.b }; // 浅拷贝
obj2.b.c = 100;
console.log(obj1.b.c); // 100 (相互影响)
  1. Object.assign()

Object.assign(target, …sources) 将源对象的可枚举属性复制到目标对象,返回目标对象。它是浅拷贝。

1
2
3
4
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = Object.assign({}, obj1);
obj2.b.c = 100;
console.log(obj1.b.c); // 100
  1. 展开运算符 …
1
2
3
4
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { ...obj1 };
obj2.b.c = 100;
console.log(obj1.b.c); // 100
  1. 数组的浅拷贝方法

数组也是对象,许多数组方法返回的是浅拷贝。

  • slice():
1
2
3
4
const arr1 = [1, 2, { a: 3 }];
const arr2 = arr1.slice();
arr2[2].a = 100;
console.log(arr1[2].a); // 100
  • concat():
1
const arr2 = arr1.concat();
  • 展开运算符:
1
const arr2 = [...arr1];
  • Array.from():
1
const arr2 = Array.from(arr1);

深拷贝的实现方式

JSON.parse(JSON.stringify(obj))

这是最常用的简单深拷贝方法,但存在一些限制:

  • 无法拷贝函数、undefined、Symbol、BigInt。
  • 无法处理循环引用(会抛出错误)。
  • 特殊对象如 Date、RegExp、Map、Set 等会被序列化为字符串或空对象。
  • 会丢弃对象的 constructor,原型链不再保留。
1
2
3
4
5
6
7
8
9
10
11
const obj1 = {
a: 1,
b: { c: 2 },
d: new Date(),
e: undefined,
f: function() {},
g: Symbol('g')
};
const obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj2);
// { a: 1, b: { c: 2 }, d: "2024-01-25T..." } // 函数、undefined、Symbol 丢失,Date 变成了字符串

递归实现深拷贝(基础版)

手动编写递归函数,处理基本类型和对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 处理数组和普通对象
const cloneObj = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key]);
}
}
return cloneObj;
}

const obj1 = { a: 1, b: { c: 2 }, d: [1, 2, { e: 3 }] };
const obj2 = deepClone(obj1);
obj2.b.c = 100;
console.log(obj1.b.c); // 2

该基础版未处理循环引用、Map、Set、Symbol 属性等,但已满足大部分场景。

处理循环引用

使用 WeakMap 记录已拷贝的对象,遇到循环引用直接返回记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (hash.has(obj)) return hash.get(obj); // 处理循环引用

const clone = Array.isArray(obj) ? [] : {};
hash.set(obj, clone);

// 处理 Symbol 属性
const symKeys = Object.getOwnPropertySymbols(obj);
if (symKeys.length) {
symKeys.forEach(symKey => {
clone[symKey] = deepClone(obj[symKey], hash);
});
}

// 处理普通字符串属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], hash);
}
}
return clone;
}

// 测试循环引用
const obj = { a: 1 };
obj.self = obj;
const cloned = deepClone(obj);
console.log(cloned.self === cloned); // true

使用第三方库

lodash 的 _.cloneDeep():功能完善,处理了各种边界情况。

1
2
const _ = require('lodash');
const obj2 = _.cloneDeep(obj1);

jQuery 的 $.extend(true, {}, obj)。

结构化克隆(Structured Clone)

现代浏览器提供 structuredClone() 全局函数,用于深拷贝,支持大部分内置类型(如 Date、RegExp、Map、Set、ArrayBuffer 等),但仍不能拷贝函数、Symbol、DOM 节点等。

1
2
3
const obj1 = { a: 1, b: { c: 2 }, d: new Date() };
const obj2 = structuredClone(obj1);
console.log(obj2); // 正确深拷贝

浅拷贝与深拷贝对比示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 原始对象
const original = {
name: 'Alice',
age: 25,
address: {
city: 'New York',
zip: 10001
},
hobbies: ['reading', 'gaming']
};

// 浅拷贝
const shallowCopy = { ...original };

// 深拷贝(JSON 方法)
const deepCopy = JSON.parse(JSON.stringify(original));

// 修改浅拷贝的嵌套对象
shallowCopy.address.city = 'Los Angeles';
shallowCopy.hobbies.push('swimming');
console.log(original.address.city); // 'Los Angeles' (原对象被改变)
console.log(original.hobbies); // ['reading', 'gaming', 'swimming'] (原对象被改变)

// 修改深拷贝的嵌套对象
deepCopy.address.city = 'Chicago';
deepCopy.hobbies.push('running');
console.log(original.address.city); // 仍为 'Los Angeles' (不受影响)
console.log(original.hobbies); // 仍为 ['reading', 'gaming', 'swimming'] (不受影响)

特殊场景与注意事项

  1. 函数和 undefined 属性:JSON.stringify 会丢失函数和 undefined,递归实现可以保留函数(拷贝函数引用),但通常函数是无状态的,浅拷贝即可。
  2. Symbol 作为属性名:JSON.stringify 会忽略 Symbol 属性,递归拷贝时可借助 Object.getOwnPropertySymbols 处理。
  3. Date、RegExp、Map、Set 等:JSON.stringify 会将其转换为字符串或普通对象,导致信息丢失。递归拷贝时应针对这些类型特殊处理。
  4. 原型链:浅拷贝和大多数深拷贝方法都不会复制原型链上的属性,只拷贝对象自身的可枚举属性。如果需要保留原型,可使用 Object.create(Object.getPrototypeOf(obj)) 并结合 Reflect.ownKeys 等方式。
  5. 性能:深拷贝递归遍历对象,可能影响性能,对于大型对象应谨慎使用,或考虑 immutable 数据结构。

总结

特性 浅拷贝 深拷贝
是否创建新对象
复制基本类型值 复制值 复制值
复制引用类型 复制引用(共享内存) 递归复制新对象
修改嵌套属性影响原对象 不会
实现复杂度 简单(内置方法) 较复杂(需递归)
常见实现 Object.assign、…、Array.slice() JSON.parse(JSON.stringify())、递归、structuredClone、lodash

选择浅拷贝还是深拷贝取决于需求:

  1. 如果对象只有一层属性(所有属性都是基本类型),浅拷贝即可。
  2. 如果对象包含嵌套结构,且希望完全隔离,必须使用深拷贝。
  3. 注意深拷贝的性能和边界情况,必要时选择成熟库或原生 structuredClone。

闭包是 javascript 中一个核心概念,但它并非孤立存在,而是与作用域、模块化、回调函数等紧密相连。

闭包与作用域

作用域决定了变量的可见性

javascript 采用词法作用域(静态作用域),即函数的作用域在定义时就已确定,而不是在执行时确定。这意味着,一个函数可以访问其外部函数中声明的变量,这种访问规则由作用域链(scope chain)实现。

闭包是作用域链的动态延续

当一个内部函数引用了外部函数的变量,并且这个内部函数在外部函数执行完毕后仍然可用(例如被返回或传递给其他函数),就形成了闭包。此时,即使外部函数已执行结束,其变量对象仍被内部函数引用,不会被垃圾回收。这正是闭包对作用域的“延长”效果。

1
2
3
4
5
6
7
8
9
function outer() {
let x = 10;
function inner() {
console.log(x); // 内部函数引用外部变量
}
return inner;
}
const closure = outer();
closure(); // 10 // 闭包保留了 outer 的作用域

关系总结

闭包是作用域链的具体体现,它让函数能够“记住”并访问其词法作用域,即使函数在当前作用域之外执行。

闭包与模块化

模块化的核心需求:封装与隐藏

模块化旨在将代码分割成独立、可复用的单元,同时隐藏内部实现细节,只暴露公共接口。在 ES6 模块出现之前,javascript 没有原生的模块系统,开发者利用闭包实现“模块模式”。

闭包实现模块化:IIFE 与返回值

通过立即执行函数表达式(IIFE)创建私有作用域,返回一个包含公共方法的对象。这些公共方法通过闭包访问私有变量,从而实现信息隐藏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const counterModule = (function() {
let count = 0; // 私有变量
function changeBy(val) {
count += val;
}
return {
increment() {
changeBy(1);
},
decrement() {
changeBy(-1);
},
getCount() {
return count;
}
};
})();

counterModule.increment();
console.log(counterModule.getCount()); // 1
console.log(counterModule.count); // undefined,无法直接访问私有变量

在这个例子中,count 和 changeBy 被封装在闭包中,外部只能通过返回的对象操作它们。这正是闭包对模块化的支持。

ES6 模块与闭包

ES6 模块(import/export)虽然语法不同,但底层仍然利用了闭包的特性。每个模块都有自己的作用域,导出内容相当于暴露了公共接口,而模块内部的变量对外部不可见,本质也是闭包的一种应用。

关系总结

闭包是实现模块化的重要工具,它通过函数作用域和变量引用来创建私有空间,为模块化提供了语言层面的基础。

闭包与回调函数

回调函数需要记住上下文

回调函数经常在异步操作(如事件监听、定时器、网络请求)中使用。当回调执行时,原始的执行上下文可能已经结束,但回调往往需要访问当时的数据。闭包正好解决了这个问题——回调函数定义时捕获的外部变量会被保留。

1
2
3
4
5
6
7
8
9
function fetchData(url) {
const requestId = Math.random(); // 模拟请求ID
setTimeout(function callback() {
console.log(`请求 ${url} 完成,ID: ${requestId}`);
}, 1000);
}

fetchData('/api/data');
// 一秒后输出:请求 /api/data 完成,ID: 0.123456...

这里的 callback 是一个闭包,它记住了 url 和 requestId,即使 fetchData 早已执行完毕。

高阶函数与闭包

许多高阶函数(如 map、filter、forEach)接受回调函数作为参数,这些回调同样可以形成闭包。

1
2
3
4
5
6
7
8
9
function createMultiplier(factor) {
return function (number) {
return number * factor; // factor 被闭包捕获
};
}

const numbers = [1, 2, 3];
const doubled = numbers.map(createMultiplier(2));
console.log(doubled); // [2, 4, 6]

createMultiplier(2) 返回的函数作为回调传给 map,它捕获了 factor 变量。

事件处理与闭包

在事件监听中,闭包常用于保存循环变量等状态。

1
2
3
4
5
6
7
const buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log(`按钮 ${i} 被点击`); // 闭包捕获 i
});
}
// 但输出总是 '按钮 3 被点击'(如果使用 var)

这就是著名的循环陷阱,因为 var 没有块级作用域,所有回调共享同一个 i。利用闭包可以修复:

1
2
3
4
5
6
7
for (var i = 0; i < buttons.length; i++) {
(function(j) {
buttons[j].addEventListener('click', function() {
console.log(`按钮 ${j} 被点击`); // 闭包捕获 j
});
})(i);
}

关系总结

回调函数常常以闭包的形式存在,因为它需要在未来某个时刻访问定义时的环境。闭包让回调变得“有记忆”,是异步编程的基础。

三者交汇:一个综合示例

考虑一个简单的计数器模块,它支持异步增量操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const counter = (function() {
let count = 0; // 闭包私有变量(模块化)

function asyncIncrement(delay) {
setTimeout(function callback() {
count++; // 回调闭包访问 count
console.log(`当前计数: ${count}`);
}, delay);
}

function getCount() {
return count;
}

return {
asyncIncrement,
getCount
};
})();

counter.asyncIncrement(1000);
counter.asyncIncrement(1000);
// 一秒后依次输出:
// 当前计数: 1
// 当前计数: 2

作用域:count 存在于模块的闭包作用域中,callback 通过作用域链访问它。
模块化:IIFE 封装了私有变量 count,只暴露 asyncIncrement 和 getCount。
回调函数:setTimeout 的回调 callback 形成了闭包,捕获了 count 和 delay 变量,实现了异步递增。

五、总结

概念 与闭包的关系
作用域 闭包是基于词法作用域产生的,它让函数可以持续访问定义时的作用域。
模块化 闭包是实现模块模式的关键技术,通过私有变量和公共接口实现封装。
回调函数 回调函数常作为闭包出现,以便在异步执行时保留所需的环境变量。

闭包不仅是 javascript 的特性,更是连接这些编程范式的重要桥梁。

闭包是指函数与其词法环境的组合,也就是内部函数可以访问外部函数作用域中的变量,即使外部函数已经执行完毕。

什么是闭包?

在 javascript 中,每当创建一个函数,就会形成闭包。更准确地说,闭包是由函数以及该函数被声明时所在的作用域共同组成的。这个作用域包含了函数内部引用的所有外部变量。

看一个最简单的闭包示例:

1
2
3
4
5
6
7
8
9
10
function outer() {
const message = 'Hello, Closure!';
function inner() {
console.log(message);
}
return inner;
}

const closureFn = outer();
closureFn(); // 输出 'Hello, Closure!'

在这个例子中,inner 函数使用了外部函数 outer 的变量 message。当 outer 执行完毕返回 inner 后,按照常理 outer 的作用域应该被销毁,但由于 inner 仍然持有对 message 的引用,javascript 引擎会保留这个变量,使得 inner 可以在外部继续访问它。这个 inner 函数及其引用的环境就构成了一个闭包。

闭包的原理

闭包的形成依赖于 javascript 的词法作用域(静态作用域)和垃圾回收机制。

  1. 词法作用域:函数的作用域在定义时就已经确定,而不是在执行时确定。因此,内部函数总能访问外部函数中声明的变量。
  2. 垃圾回收:通常,当函数执行完后,其局部变量会被标记为可回收。但如果还有别的函数(如内部函数)仍然引用这些变量,它们就不会被回收,从而继续存在。

闭包常见的应用场景

创建私有变量

javascript 没有真正的私有成员,但可以利用闭包模拟私有变量。通过函数作用域隐藏变量,只暴露特定方法进行操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function createCounter() {
let count = 0;
return {
increment() {
count++;
console.log(count);
},
decrement() {
count--;
console.log(count);
},
getCount() {
return count;
}
};
}

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
console.log(counter.getCount()); // 2
// 无法直接访问 count
console.log(counter.count); // undefined

函数工厂

闭包可以用于生成具有特定行为的函数,例如根据参数创建不同的处理函数。

1
2
3
4
5
6
7
8
9
10
function multiplyBy(factor) {
return function(number) {
return number * factor;
};
}

const double = multiplyBy(2);
const triple = multiplyBy(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15

回调函数与事件处理

在异步操作或事件监听中,闭包常用于保留状态。

1
2
3
4
5
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 输出 3 3 3
}, 100);
}

上面的例子中,由于 var 没有块级作用域,所有回调共享同一个 i,循环结束后 i 变为 3,因此输出三个 3。利用闭包可以解决这个问题:

1
2
3
4
5
6
7
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 输出 0 1 2
}, 100);
})(i);
}

或者使用 let 创建块级作用域:

1
2
3
4
5
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 输出 0 1 2
}, 100);
}

模块化

在 ES6 模块出现之前,闭包常用于实现模块模式,隔离内部实现,只暴露公共 API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var myModule = (function() {
var privateVar = 'I am private';
function privateMethod() {
console.log('private method');
}
return {
publicMethod: function() {
console.log(privateVar);
privateMethod();
}
};
})();

myModule.publicMethod(); // I am private \n private method

闭包的内存管理

由于闭包会持续引用外部函数的变量,这些变量无法被垃圾回收,如果使用不当可能导致内存泄漏。例如,在不需要闭包时,仍然保持引用:

1
2
3
4
5
6
7
function heavyProcess() {
const largeData = new Array(1000000).fill('data');
return function() {
console.log(largeData.length);
};
}
const leak = heavyProcess(); // largeData 一直存在

上例中 largeData 无法被回收,因为闭包一直引用它。如果不再需要闭包,应该将引用设为 null:

1
leak = null; // 解除引用,允许垃圾回收

闭包与性能

闭包虽然强大,但过度使用可能带来性能问题,因为每个闭包都额外维护自己的作用域链。在频繁创建大量闭包的场景(如循环中创建函数)时需谨慎,尽量重用函数。

总结

  1. 闭包 = 函数 + 函数定义时的词法环境。
  2. 内部函数可以访问外部函数的变量,即使外部函数已经返回。
  3. 常见用途:私有变量、函数工厂、回调、模块化。
  4. 注意内存管理,及时释放不再需要的闭包。
  5. 现代 javascript 中,let/const 和箭头函数可以简化某些闭包场景,但闭包的核心思想依然不变。

文件生成

通过 tsc --init 命令生成。

配置注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
{
/****** 根选项 ******/
"include": ["./src/**/*"], // 指定被编译文件所在的目录,** 表示任意目录,* 表示任意文件
"exclude": [], // 指定不需要被编译的目录
files: ["demo.ts"], // 指定被编译的文件


/****** 项目选项 ******/
"compilerOptions": {
"target": "ES6", // 目标语言的版本
"module": "commonjs", // 生成代码的模板标准
"lib": ["DOM", "ES5", "ES6", "ES7", "ScriptHost"], // TS需要引用的库
"outDir": "./dist", // 指定输出目录
"rootDir": "./", // 指定输出文件目录(用于输出),用于控制输出目录结构
"allowJs": true, // 允许编译器编译JS,JSX文件
"checkJs": true, // 允许在JS文件中报错,通常与allowJS一起使用
"removeComments": true, // 删除注释
"esModuleInterop": true, // 允许export=导出,由import from 导入

/****** 严格检查选项 ******/
"strict": true, // 开启所有严格的类型检查
"alwaysStrict": true, // 在代码中注入'use strict'
"noImplicitAny": true, // 不允许隐式的any类型
"noImplicitThis": true, // 不允许this有隐式的any类型
"strictNullChecks": true, // 不允许把null、undefined赋值给其他类型的变量
"strictBindCallApply": true, // 严格的bind/call/apply检查
"strictFunctionTypes": true, // 不允许函数参数双向协变
"strictPropertyInitialization": true, // 类的实例属性必须初始化

/****** 额外检查 ******/
"noUnusedLocals": true, //是否检查未使用的局部变量
"noUnusedParameters": true, //是否检查未使用的参数
"noImplicitReturns": true, //检查函数是否不含有隐式返回值
"noImplicitOverride": true, //是否检查子类继承自基类时,其重载的函数命名与基类的函数不同步问题
"noFallthroughCasesInSwitch": true, //检查switch中是否含有case没有使用break跳出
"noUncheckedIndexedAccess": true, //是否通过索引签名来描述对象上有未知键但已知值的对象
"noPropertyAccessFromIndexSignature": true, //是否通过" . “(obj.key) 语法访问字段和"索引”( obj[“key”]), 以及在类型中声明属性的方式之间的一致性

/****** 实验选项 ******/
"experimentalDecorators": true, //是否启用对装饰器的实验性支持,装饰器是一种语言特性,还没有完全被 JavaScript 规范批准
"emitDecoratorMetadata": true, //为装饰器启用对发出类型元数据的实验性支持

/****** 高级选项 ******/
"forceConsistentCasingInFileNames": true, //是否区分文件系统大小写规则
"extendedDiagnostics": false, //是否查看 TS 在编译时花费的时间
"noEmitOnError": true, //有错误时不进行编译
"resolveJsonModule": true, //是否解析 JSON 模块
},
}

常用配置

  1. include
    指定编译文件,默认是编译当前目录下所有的ts文件

  2. exclude
    指定排除的文件,即不编译指定的文件

  3. target
    指定编译 js 的版本,常用的有 es5 和 es6

  4. allowJS
    是否允许编译js文件

  5. removeComments
    是否在编译过程中删除文件中的注释

  6. rootDir
    编译文件的目录

  7. outDir
    输出的目录

  8. sourceMap
    代码源文件

  9. strict
    严格模式

  10. module
    默认 common.js,可选 es6模式 amd 和 umd 等

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

软件工程的一个主要部分是构建组件,这些组件不仅具有定义明确且一致的 API,而且还可以重用。能够处理今天和明天的数据的组件将为你提供构建大型软件系统的最灵活的能力。

简单写法

1
2
3
4
5
6
7
8
9
10
11
// 不使用泛型
function getName01(name: string): string {
return name
}
console.log(getName01('翠花')) // 翠花

// 使用泛型
function getName02<T>(name: T): T {
return name
}
console.log(getName02<string>('翠花')) // 翠花

从上面的例子我们可以知道,如果使用了泛型,可以在调用该函数时再指定类型。比如我们在获取名字时,发现有些人并没有使用真实姓名,而是使用了 数字代号 作为名字,这时我们应该怎么解决呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
// 不使用泛型
function getName01(name: string | number): string | number {
return name
}
console.log(getName01('翠花')) // 翠花
console.log(getName01(123)) // 123

// 使用泛型
function getName02<T>(name: T): T {
return name
}
console.log(getName02<string>('翠花')) // 翠花
console.log(getName02<number>(123)) // 123

结论:使用泛型更灵活。

泛型函数

1
2
3
4
function getName03<T>(name: T): T {
return name
}
console.log(getName03<string>('隔壁老王')) // 隔壁老王

注意:<T> 中的 T 可以是其他合法的字母,建议使用大写字母。

泛型接口

1
2
3
4
5
6
7
8
interface GetName04 {
<T>(name: T): T
}
function getName05<T>(name: T): T {
return name
}
let my_name: GetName04 = getName05
console.log(my_name('小红')) // 小红

复杂点的用法

1
2
3
4
5
6
7
8
9
interface GetName06 {
<T>(name: T): T;
<U>(age: U): U;
}
function getName07<T, U>(args: { name: T, age: U }) : { name: T, age: U } {
return args
}
let my_name_age: GetName06 = getName07
console.log(my_name_age({ name: '小红', age: 18 })) // { name: '小红', age: 18 }

泛型类

1
2
3
4
5
6
7
8
9
10
11
class Add<T> {
value: T;
addFn: (a: T, b: T) => T
}
let add_a_b = new Add<number>()
add_a_b.value = 666
console.log(add_a_b.value) // 666
add_a_b.addFn = (a, b) => {
return a + b
}
console.log(add_a_b.addFn(2, 3)) // 5

泛型约束

先看一个例子

1
2
3
4
function getName08<T>(name: T): T {
return `我是${name}` // 不能将类型“string”分配给类型“T”。
}
console.log(getName08<string>('隔壁老王'))

我们在使用泛型函数时,想要返回 我是${name} 时会报错,这是因为该函数返回的结果是字符串,而 nameT 类型,也就是未知类型,这时就会产生冲突了,有的朋友为了减少麻烦,可能会简单粗暴使用 any ,如下:

1
2
3
4
function getName08<T>(name: T): any {
return `我是${name}`
}
console.log(getName08<string>('隔壁老王')) // 我是隔壁老王

这确实可以解决问题,但是却失去了使用泛型的意义,这时最佳的解决方法是使用 泛型约束,如下:

1
2
3
4
5
6
7
interface Str {
name: String
}
function getName08<T extends Str>(person: T): any {
return `我是${person.name}呀`
}
console.log(getName08({ name: '隔壁老王'})) // 我是隔壁老王呀

使用 keyof 约束对象

我们可以声明受另一个类型参数约束的类型类型参数。如下,U (search) 继承并受到了 T (obj) 的约束,这确保我们不会获取到除 T (obj) 上不存在的属性,而是从 T (obj) 中获取其中的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
function getObj<T, U extends keyof T>(obj: T, search: U) {
return obj[search]
}
const students = {
1: { name: '张三', age: 16 },
2: { name: '李四', age: 17 },
3: { name: '王五', age: 18 },
4: { name: '赵六', age: 19 },
5: { name: '老七', age: 20 }
}
console.log(getObj(students, 3)) // { name: '王五', age: 18 }
console.log(getObj(students, 5)) // { name: '老七', age: 20 }
console.log(getObj(students, 6)) // 类型“6”的参数不能赋给类型“2 | 3 | 1 | 4 | 5”的参数。

链接:Typescript中文官网,Symbols篇

symbolES6 新增的一种基本数据类型,它和 numberstringbooleanundefinednull 是同类型的,object 是引用类型。它用来表示独一无二的值,通过 Symbol 函数生成。

创建 symbol类型

symbol类型的值是通过Symbol构造函数创建的。

1
2
const a = Symbol()
console.log(typeof a) // symbol

Symbol的值是唯一

1
2
3
let b = Symbol('key')
let c = Symbol('key')
console.log(b === c) // false

用做对象属性的键

1
2
3
4
5
let d = Symbol()
let e = {
[d]: 'value'
}
console.log(e[d]) // value

声明对象的属性和类成员

1
2
3
4
5
6
7
8
const f = Symbol()
class G {
[f]() {
return 'value'
}
}
const h = new G()
console.log(h[f]()) // value

symbol值可以转为字符串和布尔类型值

1
2
3
const i = Symbol('i')
console.log(i.toString()) // Symbol(i)
console.log(Boolean(i)) // true

注意:symbol值不可以和其他类型的值进行运算

Symbol属性名的遍历

使用 Symbol 类型值作为属性名,这个属性不会被 for…in 遍历到,也不会被 Object.keys() Object.getOwnPropertyNames() JSON.stringify() 获取到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const name = Symbol('name')
const person = {
[name]: '老王',
age: 30,
sex: '男'
}
let for_key = []
for (let key in person) {
for_key.push(key)
}
console.log(for_key) // [ 'age', 'sex' ]
console.log(Object.keys(person)) // [ 'age', 'sex' ]
console.log(Object.getOwnPropertyNames(person)) // [ 'age', 'sex' ]
console.log(JSON.stringify(person)) // {"age":30,"sex":"男"}

使用 Object.getOwnPropertySymbols() 方法获取对象的所有symbol类型的属性名。

1
2
3
4
5
6
7
const name = Symbol('name')
const person = {
[name]: '老王',
age: 30,
sex: '男'
}
console.log(Object.getOwnPropertySymbols(person)) // [ Symbol(name) ]

可以用 ES6 新提供的 Reflect 对象 的静态方法 Reflect.ownKeys,它可以返回所有类型的属性名,所以 Symbol 类型的也会返回。

1
2
3
4
5
6
7
const name = Symbol('name')
const person = {
[name]: '老王',
age: 30,
sex: '男'
}
console.log(Reflect.ownKeys(person)) // [ 'age', 'sex', Symbol(name) ]

众所周知的Symbols

  1. Symbol.hasInstance
    方法,会被instanceof运算符调用。构造器对象用来识别一个对象是否是其实例。
1
2
3
4
5
6
7
// 当其他对象使用 instanceof 判断是否为这个对象的实例时,会调用你定义的这个方法,参数是其他的这个对象
const obj01 = {
[Symbol.hasInstance](other: any) {
console.log(other) // { a: 'a' }
}
}
console.log({ a: 'a' } instanceof (obj01 as any)) // false
  1. Symbol.isConcatSpreadable
    布尔值,表示当在一个对象上调用Array.prototype.concat时,这个对象的数组元素是否可展开。
1
2
3
4
5
6
7
8
let arr01 = [1, 2]
console.log(arr01.concat([3, 4])) // [ 1, 2, 3, 4 ]
let arr02 = ['a', 'b'] as any
arr02[Symbol.isConcatSpreadable] = false
console.log(arr02.concat(['c', 'd'])) // [ [ 'a', 'b', [Symbol(Symbol.isConcatSpreadable)]: false ], 'c', 'd' ]
// 当数组arr02的 Symbol.isConcatSpreadable 设为 true 时,这个数组在数组的 concat 方法中不会被扁平化
arr02[Symbol.isConcatSpreadable] = true
console.log(arr02.concat(['e', 'f'])) // [ 'a', 'b', 'e', 'f' ]
  1. Symbol.iterator
    方法,被for-of语句调用。返回对象的默认迭代器。
1
2
3
4
5
const arr = [1, 2]
const iterator = arr[Symbol.iterator]()
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: 2, done: false }
console.log(iterator.next()) // { value: undefined, done: true }
  1. Symbol.match
    方法,被String.prototype.match调用。正则表达式用来匹配字符串。
1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
[Symbol.match](val: any) {
return val.length
}
}
console.log('abcdefg'.match(obj)) // 7

// 正则表达式用来匹配字符串
const str: any = /abcdefg/
str[Symbol.match] = false
console.log('/abc/'.startsWith(str)) // false
console.log('/abcdefg/'.endsWith(str)) // true
  1. Symbol.replace
    方法,被String.prototype.replace调用。正则表达式用来替换字符串中匹配的子串。
1
2
3
4
5
6
7
8
9
10
11
class obj02 {
value: string
constructor(value: any) {
this.value = value
}
[Symbol.replace](str: any) {
return `${str}:${this.value}`
}
}
const n: any = '姓名'
console.log(n.replace(new obj02('老王'))) // 姓名:老王
  1. Symbol.search
    方法,被String.prototype.search调用。正则表达式返回被匹配部分在字符串中的索引。

  2. Symbol.species
    函数值,为一个构造函数。用来创建派生对象。

  3. Symbol.split
    方法,被String.prototype.split调用。正则表达式来用分割字符串。

  4. Symbol.toPrimitive
    方法,被ToPrimitive抽象操作调用。把对象转换为相应的原始值。

  5. Symbol.toStringTag
    方法,被内置方法Object.prototype.toString调用。返回创建对象时默认的字符串描述。

  6. Symbol.unscopables
    对象,它自己拥有的属性会被with作用域排除在外。

TypeScript 使用 never 关键字来表示逻辑上不应该发生的情况和控制流,即不应该存在的状态。

可能出现 never 的情况

  1. 函数抛出一个错误异常
1
2
3
const error = () => {
throw new Error('error')
}
  1. 函数包含一个无限循环
1
2
3
const loop = () => {
while(true) {}
}
  1. 类型判断或类型缩小的细化(全面性检查)

在条件语句中,如果 TypeScript 能够推断出某个分支永远不会执行,那么该分支的类型会被推断为 never

1
2
3
4
5
6
7
const isTrue = (val: 'A' | 'B') => {
switch(val) {
case 'A': break;
case 'B': break;
default: const isFalse: never = val
}
}

never 和 void 的区别

  1. 如果没有为函数指定返回类型,并且在代码中没有返回任何内容,TypeScript 将推断其返回类型为 void。在TypeScript中,不返回任何内容的 void 函数实际上返回的是 undefined。

void 类型的值可以是 undefinednull

1
2
const fn = () => {}
console.log(fn()) // undefined
  1. 不能将 void 指定给 never
1
2
const fn = () => {}    // const fn: () => void
let aa: never = fn() // 不能将类型“void”分配给类型“never”

never 类型的特点

  1. never 类型会从联合类型中移除
1
2
3
4
5
6
type a = unknown | never  // unknown
type b = any | never // any
type c = number | never // number
type d = string | never // string
type e = object | never // object
type f = boolean | never // boolean
  1. never 类型与任意类型的交叉类型都是 never
1
2
3
4
5
6
type g = unknown & never  // never
type h = any & never // never
type i = number & never // never
type j = string & never // never
type k = object & never // never
type l = boolean & never // never
  1. never 可以赋值给任意类型
1
2
3
4
5
6
7
let m: never
let n: unknown = m
let o: any = m
let p: number = m
let q: string = m
let r: object = m
let s: boolean = m
  1. 其他类型不能赋值给 never
1
2
3
4
let t: never = 'a'      // 不能将类型“string”分配给类型“never”
let u: never = 123 // 不能将类型“number”分配给类型“never”
let v: never = { a: 1 } // 不能将类型“{ a: number; }”分配给类型“never”
let w: never = true // 不能将类型“boolean”分配给类型“never”