JavaScript
介绍
JavaScript
(通常缩写为JS)是一门基于 原型
和 头等函数
的 多范式
高级
解释型
编程语言,它支持 面向对象
程序设计、 指令式编程
和 函数式编程
。
它提供方法来操控文本、数组、日期以及正则表达式等。
不支持I/O,比如网络、存储和图形等,但这些都可以由它的宿主环境提供支持。
它由Ecma
通过ECMAScript
实现语言的标准化。
目前,它被世界上的绝大多数网站所使用,也被世界主流浏览器(Chrome、Firefox、Safari和Opera)所支持。
JavaScript与Java在名字和语法上都很相似,但这两门编程语言从设计之初就有很大不同。 JavaScript在语言设计上主要受到了Self
(一种基于原型的编程语言)和Scheme
(一门函数式编程语言)的影响, 在语法结构上它和C语言很相似(如if
条件语句、switch
语句、while
循环和do-while
循环等)。
ECMAScript
JavaScript
是语言,ECMAScript
是规范。
JavaScript
是 ECMAScript
的实现之一(还有如 JScript
、ActionScript
)
几个重要的ECMAScript
版本:
- ES3(1999)
- 第一个广泛支持的版本
- 引入:正则表达式、异常处理(try-catch)、switch、do...while
- ES5(2009)
- 大规模应用前的标准,兼容性强
- 特性:
- "use strict" 严格模式
- Array.prototype.forEach/map/filter
- Object.defineProperty
- JSON.parse / JSON.stringify
- ES6 / ECMAScript 2015
- JavaScript 的一次重大变革
- 引入现代开发核心语法:
- let / const
- 箭头函数 ()=>{}
- 类(class)、模块(import/export)
- 解构赋值、模板字符串、默认参数
- Promise
- Map / Set
- ES8 / ECMAScript 2017
- 引入了 异步编程革命
- 特性:
- async/await(写异步代码像同步代码)
- Object.entries / Object.values
- ES11 / ECMAScript 2020
- 现代语法糖丰富
- 特性:
- 可选链操作符 ?.
- 空值合并操作符 ??
- Promise.allSettled
- 动态 import()
基本用法
插入方式
- 内联方式
<button onclick="alert('点击了按钮')">点我</button>
- 内部脚本
<script>
console.log('页面加载完成');
</script>
- 外部引入(推荐做法)
<script src="main.javascript"></script>
推荐引入方式
推荐 HTML
末尾引入外部 JS
<body>
<!-- 页面内容 -->
<script src="main.javascript"></script> <!-- 放在 body 末尾 -->
</body>
优点:
提升页面加载速度 浏览器解析
HTML
是自上而下的,如果JS
写在前面,会阻塞DOM
构建。而写在后面可以先加载内容,提升用户体验。确保
DOM
元素已加载 当 JS 执行时,页面中的元素已经加载完成,避免出现document.getElementById(...) is null
这类错误。无需等待或写
DOMContentLoaded
不用额外包装代码在:
document.addEventListener("DOMContentLoaded", () => {
// code here
});
若放在 <head>
中,使用 defer
延迟执行,等 DOM 完成后才运行。
<script src="main.javascript" defer></script>
执行过程
HTML
解析 → 浏览器解析HTML
,从上到下构建DOM
树;- 遇到
<script>
标签 → 浏览器暂停HTML
解析,执行JS
脚本; - 同步执行代码 →
JavaScript
是单线程的,按顺序同步执行; - 操作
DOM
或响应事件 → 可以通过JS
修改页面内容、响应用户操作; - 异步任务(如
setTimeout
、fetch
) → 放入任务队列,等待主线程空闲时执行。
变量
在 JavaScript 中,变量是用于存储数据值的容器。理解变量是 JS 编程的基础之一。
JS 中声明变量的三种方式
关键字 | 作用域 | 是否可重复声明 | 是否可修改值 | 是否提升 | 说明 |
---|---|---|---|---|---|
var | 函数作用域 | ✅ 是 | ✅ 是 | ✅ 是 | 早期使用方式,灵活但易出错 |
let (ES6) | 块级作用域 | ❌ 否 | ✅ 是 | ❌ 否 | 推荐用于可变变量 |
const (ES6) | 块级作用域 | ❌ 否 | ❌ 否(值不能变) | ❌ 否 | 推荐用于常量或不可变引用 |
变量的使用示例
// var 示例(不推荐)
var x = 5;
var x = 10; // 允许重复声明
// let 示例(推荐)
let a = 3;
// let a = 4; // 报错:不能重复声明
a = 4; // 可修改
// const 示例(推荐用于常量)
const PI = 3.14;
// PI = 3.1415; // 报错:不可修改
作用域
函数作用域(var
)
function test() {
var message = "hello";
}
console.log(message); // 报错,message 在函数外不可访问
块级作用域(let
和 const
)
{
let score = 100;
}
// console.log(score); // 报错,score 在块外不可访问
变量提升(仅 var
)
var
声明的变量会被提升(Hoisting)到当前作用域的顶部,这个“作用域”可以是:
- 当前的 函数作用域(如果
var
在函数内声明) - 当前的 全局作用域(如果
var
在函数外声明)
console.log(a); // undefined (已声明但未赋值)
var a = 5;
等价于:
var a;
console.log(a);
a = 5;
let
和const
也会提升,但它们的初始化不会被提升。在声明之前访问这些变量会导致ReferenceError。,访问会直接报错(称为“暂时性死区”)。
推荐用法
推荐使用
let
代替var
var
会“泄露”出当前代码块,导致变量被意外访问或污染。let
不允许重复声明同名变量(更安全)var
会被提升到作用域顶部,容易造成未定义变量被错误使用。let
为每次循环创建了新的作用域,var
只创建一次作用域。
比如:使用
var
时for
循环中的异步操作出错javascriptfor (var i = 0; i < 3; i++) { setTimeout(function () { console.log("var i:", i); }, 100); } //输出 //1. var i: 3 //2. var i: 3 //3. var i: 3 // 原因: 1. 循环 0, i = 0; => 2. 把 setTimeout(() => console.log(i)) 放入宏任务队列 // => 3. 循环 1, i = 1; => 4. 把 setTimeout(() => console.log(i)) 放入宏任务队列 // => 5. 循环 2, i = 2; => 6. 把 setTimeout(() => console.log(i)) 放入宏任务队列 // => 7. i++,变为 3,不再满足循环条件,循环结束 // => 8. 主线程空闲,宏任务队列执行, 输出 var i: 3; var i: 3; var i: 3;
应该使用:
javascriptfor (let i = 0; i < 3; i++) { setTimeout(function () { console.log("let i:", i); }, 100); } //输出 //1. let i: 0 //2. let i: 1 //3. let i: 2 // 原因: 1. 循环 0, 创建新的块作用域, 创建 let i = 0; => 2. 把 setTimeout(() => console.log(i)) (捕获 i=0) 放入宏任务队列 // => 3. 循环 1, 创建新的块作用域, 创建 let i = 1; => 4. 把 setTimeout(() => console.log(i)) (捕获 i=1) 放入宏任务队列 // => 5. 循环 2, 创建新的块作用域, 创建 let i = 2; => 6. 把 setTimeout(() => console.log(i)) (捕获 i=2) 放入宏任务队列 // => 7. i++,变为 3,不再满足循环条件,循环结束 // => 8. 主线程空闲,宏任务队列执行, 输出 let i: 0; let i: 1; let i: 2;
也可以使用 使用 var + IIFE(立即执行函数表达式):
javascriptfor (var i = 0; i < 3; i++) { (function (j) { setTimeout(() => { console.log("var+j i:", j); }, 0); })(i); } //输出 //1. var+j i: 0 //2. var+j i: 1 //3. var+j i: 2 // 原因 // 每次循环时,调用一个立即执行函数,将 i 的值传进去 // 这个 j 是参数,属于 IIFE 的局部变量 // 回调闭包捕获的是 j,不会被后续循环影响
用
const
声明不会改变的值(如配置信息、常量)避免重复声明变量
避免不使用
var
、let
或const
来声明变量而直接赋值,会被自动变成window全局变量(在非严格模式下)
数据类型
JavaScript 的数据类型分为 基本类型(primitive types) 和 引用类型(reference types)。
基本类型(Primitive Types)
基本类型是不可变、按值传递的。
类型 | 示例 | 说明 |
---|---|---|
number | 42 , 3.14 , NaN , Infinity | 所有数字,包含整数和浮点数 |
string | 'hello' , "world" | 字符串 |
boolean | true , false | 布尔值 |
undefined | let x; → x === undefined | 未赋值时的默认值 |
null | null | 表示“空值”或“无对象” |
bigint | 1234567890123456789012345n | 超过 Number.MAX_SAFE_INTEGER 的整数 |
symbol | Symbol('id') | 创建独一无二的值(常用于对象属性) |
基本类型是不可变的值类型:
x = y
赋值后两者互不影响。
四舍五入
场景 | 用法 |
---|---|
四舍五入整数 | Math.round(num) |
保留 n 位小数 | Math.round(num * 10^n) / 10^n |
转字符串保留小数 | num.toFixed(n) |
精确金融运算(精度丢失) | decimal.javascript 等库 |
银行家舍入 | 自定义函数 |
toString
在 JavaScript 中,toString()
方法可以将对象、数组、数值、函数等转为字符串,但不同类型的 toString()
行为和格式是不同的,下面是详细的分类讲解与格式说明:
- 基本数据类型的
toString()
格式
- 数字类型
let num = 255;
console.log(num.toString()); // "255"
console.log(num.toString(2)); // "11111111"(二进制)
console.log(num.toString(16)); // "ff"(十六进制)
Number.prototype.toString([radix])
radix
是进制(2–36),默认为 10
- 布尔类型
true.toString(); // "true"
false.toString(); // "false"
- 字符串类型
字符串调用 toString()
不变:
"hello".toString(); // "hello"
- 引用类型的
toString()
格式
- 数组
[1, 2, 3].toString(); // "1,2,3"
[null, undefined].toString(); // ","
[].toString(); // ""
- 等价于
Array.prototype.join(',')
- 对象
({ a: 1 }).toString(); // "[object Object]"
默认行为是:
Object.prototype.toString.call(value); // 标准类型标签
例如:
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(new Date()); // "[object Date]"
- 函数
function greet() { return "hi"; }
console.log(greet.toString());
输出函数的源码字符串:
"function greet() { return \"hi\"; }"
- Date 对象
new Date().toString(); // e.g. "Tue May 13 2025 17:40:00 GMT+0800 (China Standard Time)"
方法名 | 返回值示例 | 描述 |
---|---|---|
toString() | "Tue May 13 2025 20:30:00 GMT+0800 (CST)" | 本地时间字符串(默认) |
toDateString() | "Tue May 13 2025" | 本地日期(无时间) |
toTimeString() | "20:30:00 GMT+0800 (CST)" | 本地时间(无日期) |
toUTCString() | "Tue, 13 May 2025 12:30:00 GMT" | UTC 格式字符串 |
toISOString() | "2025-05-13T12:30:00.000Z" | ISO 8601 格式(UTC) |
toLocaleString() | "2025/5/13 20:30:00" (取决于地区) | 本地化日期+时间 |
- 自定义对象的
toString()
方法
你可以自定义对象的 toString()
方法:
const person = {
name: "Alice",
toString() {
return `Person: ${this.name}`;
}
};
console.log(person + ""); // "Person: Alice"
- 原型链上的
toString()
差异
所有对象继承自 Object.prototype
,如果你没有覆写 toString()
,就会默认返回 "[object Object]"
。你可以使用:
Object.prototype.toString.call(value);
来获取准确的类型标签:
值 | 返回 |
---|---|
[] | "[object Array]" |
{} | "[object Object]" |
null | "[object Null]" |
undefined | "[object Undefined]" |
1 | "[object Number]" |
/regex/ | "[object RegExp]" |
symbol
特点 | 说明 |
---|---|
唯一性 | 每次调用 Symbol() 都会生成一个独一无二的值 |
不可隐式转换字符串 | 与字符串拼接时会报错,只能显式转成字符串 |
可作为对象键 | 可用作对象属性的唯一键,不会与其他键冲突 |
不可枚举 | 用 for...in 、Object.keys() 等方法无法获取 Symbol 键 |
支持全局注册表 | 通过 Symbol.for() 实现跨文件共享同一个 Symbol 值 |
示例:
// 创建 symbol
const id1 = Symbol('id');
const id2 = Symbol('id');
console.log(id1 === id2); // false(唯一性)
// 用作对象键
const user = {
[id1]: 123
};
console.log(user[id1]); // 123
// Symbol 不能和字符串自动拼接
// console.log("User ID is: " + id1); // ❌ TypeError
console.log("User ID is: " + id1.toString()); // ✅
Symbol.for() vs Symbol()
const a = Symbol.for('key');
const b = Symbol.for('key');
console.log(a === b); // true(从全局注册表获取)
const x = Symbol('key');
const y = Symbol('key');
console.log(x === y); // false(每次唯一)
内置symbol(Well-known Symbols)
内置 Symbol
是 JavaScript
提供的一组预定义 Symbol
值,它们挂在全局 Symbol
对象上,用于改变对象的默认行为或与语言底层机制交互。
内置 Symbol | 类型/接口 | 用途简述 |
---|---|---|
Symbol.iterator | 可迭代协议 | 对象支持 for...of 、展开运算符 |
Symbol.asyncIterator | 异步可迭代协议 | 支持 for await...of 的异步迭代器 |
Symbol.toPrimitive | 类型转换 | 控制对象转换为原始值时的行为 |
Symbol.toStringTag | 类型标签 | 控制 Object.prototype.toString.call(obj) 的返回值 |
Symbol.hasInstance | instanceof 运算符 | 控制某个对象被 instanceof 判断时的结果 |
Symbol.isConcatSpreadable | 数组操作 | 控制 concat() 时是否将对象“展开” |
Symbol.species | 构造函数派生 | 控制像 .map() 这样的方法返回什么构造函数创建的对象 |
Symbol.match | 字符串匹配 | 自定义 str.match(obj) 行为 |
Symbol.replace | 字符串替换 | 自定义 str.replace(obj) 行为 |
Symbol.search | 字符串搜索 | 自定义 str.search(obj) 行为 |
Symbol.split | 字符串拆分 | 自定义 str.split(obj) 行为 |
Symbol.unscopables | with 语句 | 指定对象属性在 with 环境中是否可用(一般不推荐使用) |
示例:
Symbol.iterator
定义对象的默认迭代器,让对象可用于 for...of
、展开语法等。
const myIterable = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
}
};
console.log([...myIterable]); // [1, 2, 3]
Symbol.toPrimitive
控制对象到原始类型(如数字或字符串)的转换行为。
const obj = {
[Symbol.toPrimitive](hint) {
return hint === "number" ? 42 : "custom";
}
};
console.log(+obj); // 42
console.log(`${obj}`); // "custom"
Symbol.toStringTag
定义对象的类型标签,影响 Object.prototype.toString.call()
的输出。
const obj = {
[Symbol.toStringTag]: "MyTag"
};
console.log(Object.prototype.toString.call(obj)); // "[object MyTag]"
Symbol.hasInstance
控制对象是否为某个构造函数的“实例”。
class MyClass {
static [Symbol.hasInstance](instance) {
return instance.name === "special";
}
}
console.log({ name: "special" } instanceof MyClass); // true
Symbol.isConcatSpreadable
决定对象在 concat()
中是否被“打平”。
const arrLike = {
0: "a",
1: "b",
length: 2,
[Symbol.isConcatSpreadable]: true
};
console.log(["x"].concat(arrLike)); // ["x", "a", "b"]
Symbol.species
控制子类方法如 map()
、filter()
等返回的构造函数。
class MyArray extends Array {
static get [Symbol.species]() {
return Array;
}
}
const a = new MyArray(1, 2, 3);
const b = a.map(x => x * 2);
console.log(b instanceof MyArray); // false
console.log(b instanceof Array); // true
Symbol.match
/replace
/search
/split
自定义字符串方法行为。
const matcher = {
[Symbol.match](str) {
return str.includes("test");
}
};
console.log("This is a test".match(matcher)); // true
Symbol.unscopables
定义 with
环境中哪些属性应被忽略(不推荐使用 with
)。
const obj = {
foo: 1,
[Symbol.unscopables]: { foo: true }
};
引用类型(Reference Types)
引用类型是可变的,存的是对象的地址引用。
类型 | 示例 | 说明 |
---|---|---|
Object | {name: 'Alice'} | 所有对象的基类 |
Array | [1, 2, 3] | 数组 |
Function | function() {} | 函数本质上是对象 |
Date | new Date() | 日期对象 |
RegExp | /\d+/ | 正则表达式 |
Map | new Map() | 键值对集合(键可为任意值) |
Set | new Set() | 不重复值集合 |
引用类型是按引用传递,多个变量可以指向同一对象。
Date(日期对象)
const now = new Date();
console.log(now.toISOString()); // 当前时间 ISO 格式
// 创建指定日期
const d = new Date("2025-05-21");
// 获取日期组件
console.log(d.getFullYear(), d.getMonth() + 1, d.getDate());
// 设置日期组件
d.setFullYear(2030);
类型判断方法
JavaScript 是动态类型语言,变量类型在运行时决定,因此:
- 类型判断在调试、数据验证、类型保护、函数参数处理等场景非常关键。
- 不同类型的判断方式适用场景不同,选择错误可能会导致 bug。
typeof
— 判断基础类型
typeof 123 // "number"
typeof 'abc' // "string"
typeof true // "boolean"
typeof undefined // "undefined"
typeof Symbol() // "symbol"
typeof BigInt(1) // "bigint"
typeof function(){} // "function"
❗ 陷阱:
typeof null // "object" ❌ 历史 bug
typeof [] // "object" ❌
instanceof
— 判断引用类型是否由某构造函数创建
[] instanceof Array // true
{} instanceof Object // true
new Date() instanceof Date // true
(() => {}) instanceof Function // true
❗ 限制:
- 无法判断基础类型(例如
123 instanceof Number
为 false) - 不适合跨 iframe、window 判断(构造函数不同)
Object.prototype.toString.call(val)
— 万能判断法
这是最准确的方式。
Object.prototype.toString.call(123); // [object Number]
Object.prototype.toString.call('abc'); // [object String]
Object.prototype.toString.call(null); // [object Null]
Object.prototype.toString.call(undefined); // [object Undefined]
Object.prototype.toString.call([]); // [object Array]
Object.prototype.toString.call({}); // [object Object]
Object.prototype.toString.call(() => {}); // [object Function]
✅ 优势:
- 可判断任意数据类型
- 最精确,不受构造函数影响
Array.isArray()
— 判断数组的推荐方式
Array.isArray([]); // true
Array.isArray({}); // false
Array.isArray('abc'); // false
比 instanceof Array
更稳健(能跨 iframe 判断)。
.constructor
— 查看构造函数
(123).constructor === Number // true
'abc'.constructor === String // true
[].constructor === Array // true
({}).constructor === Object // true
❗ 风险: 可被人为更改:
const arr = [];
arr.constructor = Object;
arr.constructor === Array; // false ❌
推荐组合方案
目标 | 推荐方式 |
---|---|
判断基础类型 | typeof |
判断是否为数组 | Array.isArray() |
判断 null | val === null |
判断引用类型准确类型 | Object.prototype.toString.call(val) |
判断某类实例 | val instanceof Constructor |
函数封装
function getType(val) {
return Object.prototype.toString.call(val).slice(8, -1);
}
// 示例:
getType(null); // "Null"
getType([]); // "Array"
getType({}); // "Object"
getType(() => {}); // "Function"
getType(new Date()); // "Date"
跨 iframe/window 类型
每个 iframe
或 window
中都有自己的 JavaScript
全局环境(也就是自己的 globalThis
、window
、Object
、Array
等构造函数)。
所以:iframe1.Array !== iframe2.Array
, 虽然你在两个窗口中都写了 new Array(),它们是不同的构造函数实例。
<!-- 假设在主页面中引用了一个 iframe -->
<iframe id="myFrame" src="iframe.html"></iframe>
<script>
const iframeWin = document.getElementById('myFrame').contentWindow;
const arr = new iframeWin.Array();
arr instanceof Array; // false (与当前的window是不同的构造函数)
</script>
由于 instanceof
比较的是 构造函数的引用地址,会在跨 window
场景失败,因此应该使用:Object.prototype.toString.call(arr);
这个方法只看内部 [[Class]] 标签,不受构造函数影响,因此跨上下文判断是安全的
类型转换
JavaScript 的类型转换是一个核心概念,因为它是一门动态类型语言,变量的数据类型可以自动转换。类型转换可以分为两大类:
- 显式类型转换(Explicit Conversion)
开发者通过代码手动进行类型转换。
例如:
Number("123") // 123
String(123) // "123"
Boolean(0) // false
- 隐式类型转换(Implicit Conversion)
JavaScript 引擎在运行时自动进行的转换,也叫类型强制转换(Type Coercion)。
例如:
"5" + 1 // "51"(1 被转成字符串)
"5" - 1 // 4 ("5" 被转成数字)
true + 1 // 2 (true 转为 1)
转换规则
- 转为字符串(String)
- 显式:
String(123) // "123"
String(true) // "true"
String(null) // "null"
String(undefined) // "undefined"
String({}) // "[object Object]"
- 隐式:
123 + "abc" // "123abc"
true + "test" // "truetest"
加号
+
运算符涉及字符串时,会触发转字符串。
- 转为数字(Number)
- 显式:
Number("123") // 123
Number("abc") // NaN
Number(true) // 1
Number(false) // 0
Number(null) // 0
Number(undefined) // NaN
- 隐式:
"6" * 2 // 12
"6" - 1 // 5
true + 1 // 2
null + 1 // 1
除了加号
+
,其他数学运算符都会触发数字转换。
- 转为布尔值(Boolean)
- 显式:
Boolean(0) // false
Boolean("") // false
Boolean(null) // false
Boolean(undefined) // false
Boolean(NaN) // false
Boolean([]) // true
Boolean({}) // true
- 隐式:
在以下场景中,会触发布尔转换:
if (value)
while (value)
- 三元运算符:
value ? a : b
- 逻辑运算符:
&&
、||
、!
示例:
if ("hello") { console.log("yes"); } // 输出 "yes"
- 对象到原始值的转换(ToPrimitive)
当对象参与运算(比如加法或比较),会尝试转为原始值。
转换顺序:
Symbol.toPrimitive
- 先调用
obj.valueOf()
,如果是原始值就返回; - 否则再调用
obj.toString()
; - 如果仍不是原始值,就报错。
示例:
let obj = {
valueOf() { return 42; },
toString() { return "hello"; }
};
obj + 1 // 43
空对象 {}
的默认行为
console.log([]); // ""
const obj = {};
console.log(obj.valueOf()); // 返回自身对象:{}
console.log(obj.toString()); // "[object Object]"
{} + 1 // → "[object Object]" + 1 → "[object Object]1"
//注意,下面情况 JS 解释器把 {} 解析为代码块,这时它就被忽略了
{} + [] // → [] → ""
//想明确表示它是对象,必须加括号
({} + []) // → "[object Object]"
- 特殊值转换行为
值 | 转为 Boolean | 转为 Number | 转为 String |
---|---|---|---|
undefined | false | NaN | "undefined" |
null | false | 0 | "null" |
true | true | 1 | "true" |
false | false | 0 | "false" |
"" | false | 0 | "" |
"123" | true | 123 | "123" |
"abc" | true | NaN | "abc" |
[] | true | 0 | "" |
[1,2] | true | NaN | "1,2" |
{} | true | NaN | "[object Object]" |
==(宽松等于)和 ===(严格等于)的区别
==
会进行类型转换:
0 == false // true
"" == false // true
null == undefined // true
===
不进行类型转换:
0 === false // false
"" === false // false
null === undefined // false
常见面试题
尽量使用 ===
!!!
[] == false // true
// [] == false
// → [] 转成原始值 '' → '' == false
// → '' 转成数字 0,false 也转成 0 → 0 == 0
[] == ![] // true
// [] == ![]
// → ![] 是 false,所以变成 [] == false(同上)
null == undefined // true
// 对象与对象比较的是引用,不是值。
{} == {}; // false
[] == []; // false
// NaN 不等于自身,是 JS 中少数需要特判的值
NaN === NaN // false
isNaN(NaN) // true
Number.isNaN(NaN) // true(更严格)
运算符
JavaScript 的运算符(operators)是构建表达式和控制程序逻辑的核心工具。它们可以操作数值、字符串、对象等不同类型的数据。下面是 JS 中常见运算符的分类和用法讲解。
算术运算符(Arithmetic Operators)
用于执行数学计算:
运算符 | 含义 | 示例 | 结果 |
---|---|---|---|
+ | 加法 | 3 + 2 | 5 |
- | 减法 | 3 - 2 | 1 |
* | 乘法 | 3 * 2 | 6 |
/ | 除法 | 3 / 2 | 1.5 |
% | 取模(余数) | 5 % 2 | 1 |
** | 幂运算 | 2 ** 3 | 8 |
++ | 自增 | a++ | 先返回 a,再加 1 |
-- | 自减 | --a | 先减 1,再返回 a |
赋值运算符(Assignment Operators)
用于给变量赋值或更新变量值:
运算符 | 含义 | 示例 | 等同于 |
---|---|---|---|
= | 赋值 | x = 5 | |
+= | 加并赋值 | x += 2 | x = x + 2 |
-= | 减并赋值 | x -= 3 | x = x - 3 |
*= | 乘并赋值 | x *= 4 | x = x * 4 |
/= | 除并赋值 | x /= 2 | x = x / 2 |
%= | 取模并赋值 | x %= 2 | x = x % 2 |
**= | 幂并赋值 | x **= 3 | x = x ** 3 |
比较运算符(Comparison Operators)
用于判断两个值之间的关系,结果为布尔值(true 或 false):
运算符 | 含义 | 示例 | 结果 |
---|---|---|---|
== | 相等(类型转换) | '5' == 5 | true |
!= | 不相等 | '5' != 5 | false |
=== | 全等(值和类型) | '5' === 5 | false |
!== | 不全等 | '5' !== 5 | true |
> | 大于 | 5 > 3 | true |
< | 小于 | 5 < 3 | false |
>= | 大于等于 | 5 >= 5 | true |
<= | 小于等于 | 5 <= 3 | false |
逻辑运算符(Logical Operators)
用于多条件判断或控制表达式执行:
运算符 | 含义 | 示例 | 说明 |
---|---|---|---|
&& | 与(and) | a && b | a 为真则返回 b,否则返回 a |
|| | 或(or) | a || b | a 为真则返回 a,否则返回 b |
! | 非(not) | !true | 取反:false |
逻辑运算符的短路行为
&&
和 ||
不一定返回布尔值,而是返回最后计算的操作数。
console.log(0 || "default"); // "default"
console.log("hello" && 123); // 123
console.log(false && "fail"); // false
位运算符(Bitwise Operators)
用于对二进制位进行操作(一般用于底层优化或特殊逻辑):
运算符 | 含义 | 示例 | 说明 | |||
---|---|---|---|---|---|---|
& | 按位与 | 5 & 3 | 0101 & 0011 = 0001 | |||
` | ` | 按位或 | `5 | 3` | `0101 | 0011 = 0111` |
^ | 按位异或 | 5 ^ 3 | 0101 ^ 0011 = 0110 | |||
~ | 按位取反 | ~5 | ~00000101 = 11111010 (负数) | |||
<< | 左移 | 5 << 1 | 1010 (乘以 2) | |||
>> | 右移 | 5 >> 1 | 2 (除以 2) | |||
>>> | 无符号右移 | -5 >>> 1 | 得到正整数 |
三元运算符(三目运算符)
const result = condition ? value1 : value2;
示例:
let age = 18;
let type = age >= 18 ? "adult" : "child"; // "adult"
typeof 和 instanceof 运算符
运算符 | 含义 | 示例 |
---|---|---|
typeof | 返回数据类型字符串 | typeof "abc" → "string" |
instanceof | 判断对象类型 | arr instanceof Array → true |
解构与展开(ES6+)
虽然不属于传统运算符,但也像运算符使用:
- 解构赋值:
//数组解构
const [a, b] = [1, 2];
const [x, , y] = [10, 20, 30]; // x=10, y=30
//对象结构
const { x, y } = { x: 1, y: 2 };
//使用别名
const { name: userName } = person;
//使用默认值
const { gender = "unknown" } = person;
//undefined会被默认值覆盖但null不会
const [a = 1] = [undefined]; // a = 1
const [b = 2] = [null]; // b = null(不是默认值)
- 展开运算符:
//数组展开
const arr = [1, 2, 3];
const newArr = [...arr, 4, 5]; // [1, 2, 3, 4, 5]
//克隆数组
const copy = [...arr];
//组合多个数组
const all = [...arr1, ...arr2];
//对象展开
const obj = { a: 1, b: 2 };
const copy = { ...obj }; // 克隆
const merged = { ...obj, c: 3 }; // 合并新字段
//冲突覆盖
const o1 = { a: 1 };
const o2 = { a: 2, b: 3 };
const merged = { ...o1, ...o2 }; // { a: 2, b: 3 }
//对象展开拷贝只有一层
const o1 = { a: { b: 1 } };
const o2 = { ...o1 };
o2.a.b = 2;
console.log(o1.a.b); // 2
表达式
在 JavaScript 中,**表达式(Expression)是任何能产生值(value)**的代码。它与“语句”不同 —— 语句是“做某事”,而表达式是“返回某个值”。
什么是表达式
一个表达式是可以被计算(evaluated)并且返回一个值的代码单位,例如:
3 + 4 // 表达式,结果是 7
"hello" // 表达式,结果是 "hello"
x = 5 // 表达式,结果是 5(赋值表达式)
myFunction() // 表达式,返回函数执行结果
表达式的类型
原始值表达式
直接使用字面量:
42 // 数字
"JS" // 字符串
true // 布尔
null // null
undefined // undefined
字面量
**字面量(Literal)**是指在代码中直接写出的、表示固定值的写法。
简单来说,字面量就是你写在代码里的值本身,不需要计算或函数调用。
变量表达式
访问变量名就是表达式:
let x = 10;
x; // 表达式,结果是 10
算术表达式
返回数学运算结果:
3 + 5 * 2 // 13
(10 - 2) / 4 // 2
赋值表达式
赋值操作本身是表达式,会返回被赋的值:
let y;
y = 7 // 表达式,值是 7
let z = (y = 3); // z = 3
逻辑表达式
&&
、||
、!
是逻辑运算符:
true && false // false
"hello" || "world" // "hello"
常用于默认值判断:
let name = inputName || "Guest";
比较表达式
返回布尔值的表达式:
5 > 3 // true
"a" === "a" // true
函数表达式
- 匿名函数表达式:
const greet = function(name) {
return "Hi, " + name;
};
- 箭头函数表达式:
const add = (a, b) => a + b;
:::
调用表达式
调用函数并返回值:
sayHello() // 返回函数结果
Math.max(3, 7) // 返回 7
数组/对象表达式
[1, 2, 3] // 数组表达式
{ name: "JS" } // 对象表达式
表达式 vs 语句的区别
项目 | 表达式 | 语句 |
---|---|---|
定义 | 产生值的代码 | 执行动作的代码 |
示例 | 3 + 4 | let x = 3 + 4; |
返回值 | 有返回值 | 无返回值 |
用法 | 可嵌套在语句中 | 不可嵌套在表达式中 |
特殊表达式:条件(三元)表达式
let result = (score >= 60) ? "Pass" : "Fail";
这是一种表达式,不是语句,返回一个值。
语句
在 JavaScript 中,**语句(statement)**是用来执行特定操作的代码单位。每一条语句告诉浏览器执行一件事,比如声明变量、判断条件、循环、调用函数等。
声明语句
用于创建变量、常量或函数。
var
、let
、const
:声明变量或常量
let name = "Alice";
const PI = 3.14;
var age = 30;
function
:声明函数
function greet() {
console.log("Hello!");
}
表达式语句
表达式可以被当作语句使用(即表达式后加分号)。
x = 5 + 3;
console.log(x);
条件语句
根据条件执行不同的代码块。
if
/else if
/else
if (score > 90) {
console.log("Excellent!");
} else if (score > 60) {
console.log("Passed.");
} else {
console.log("Failed.");
}
switch
switch (day) {
case 1: console.log("Monday"); break;
case 2: console.log("Tuesday"); break;
default: console.log("Unknown");
}
循环语句
反复执行代码块。
for
for (let i = 0; i < 5; i++) {
console.log(i);
}
while
let i = 0;
while (i < 5) {
console.log(i);
i++;
}
do...while
let i = 0;
do {
console.log(i);
i++;
} while (i < 5);
for...in
/for...of
for (let key in obj) { ... }
for (let item of array) { ... }
跳转语句
控制流程转向。
break
:跳出循环或switch
continue
:跳过本次循环return
:从函数返回throw
:抛出异常
异常处理语句
处理程序运行时的错误。
try {
riskyFunction();
} catch (error) {
console.error("Error occurred:", error);
} finally {
console.log("Cleanup code.");
}
空语句
只有一个分号,什么都不做,偶尔用于占位。
;
提示
语句通常以分号(;
)结尾,虽然 JavaScript 有自动分号插入机制,但最好显式写出分号以避免意外错误。
函数
在 JavaScript 中,函数(Function)是核心概念之一,是用来封装可重用代码块的机制。它是 JavaScript 的一等公民,可以像变量一样传递、赋值、嵌套。
函数的定义方式
1. 函数声明(Function Declaration)
function greet(name) {
return "Hello, " + name;
}
函数提升
JavaScript
引擎在代码执行前,会先预处理 变量 和 函数 的 声明,把它们提升到作用域的顶部。
foo(); // "hello"
function foo() {
console.log("hello");
}
2. 函数表达式(Function Expression)
const greet = function(name) {
return "Hello, " + name;
};
函数表达式不会提升
bar(); // TypeError: bar is not a function
var bar = function () {
console.log("hi");
};
//var bar 被提升(值为 undefined)但函数赋值在运行阶段才赋上去
3. 箭头函数(Arrow Function)
const greet = name => "Hello, " + name;
- 箭头函数的
this
指向
普通函数的 this
是调用时绑定,动态决定;
而箭头函数不会创建自己的 this
,而是继承外层作用域的 this
。
不建议用箭头函数作为对象方法,因为 this 不是指向对象,而是定义时的外层上下文。
function Timer() {
this.seconds = 0;
setInterval(() => {
this.seconds++;
console.log(this.seconds); // 正常访问 Timer 实例的 this
}, 1000);
}
new Timer();
const obj = {
count: 10,
doSomethingLater: function () {
setTimeout(() => {
console.log(this.count);
}, 1000);
}
};
obj.doSomethingLater(); //输出 10,因为箭头函数继承了 doSomethingLater 的 this,即 obj。
const obj1 = {
count: 10,
doSomethingLater: function () {
setTimeout(function () {
console.log(this.count);
}, 1000);
}
};
obj1.doSomethingLater(); //输出 function,因为普通函数继动态绑定this,可以使用.bind(this)或者使用 self 或 that 保存 this 实现类似功能。
- 箭头函数不能作为构造函数(不能用 new)
箭头函数没有 [[Construct]]
内部方法,没有 prototype 属性。
const Person = (name) => {
this.name = name;
};
const p = new Person('Tom'); // ❌ TypeError: Person is not a constructor
- 箭头函数没有 arguments 对象
箭头函数中访问不到 arguments,可以用 rest 参数代替。
const logArgs = (...args) => {
console.log(args);
};
- 箭头函数不能使用 yield,因此不能定义为 generator 函数
const gen = *() => {
yield 1;
yield 2;
yield 3;
} // ❌ SyntaxError
- 箭头函数的返回值
const add = (a, b) => { a + b };
console.log(add(2, 3)); //输出 undefined。箭头函数使用大括号时必须写 return,否则默认不返回值。
4. 匿名函数(Anonymous Function)
没有名字的函数,多用于回调。
setTimeout(function() {
console.log("Hello");
}, 1000);
5. 立即执行函数(IIFE)
立即执行函数(Immediately Invoked Function Expression,)是一个被立即调用的函数表达式(不是函数声明),一般用来创建作用域。
(function() { ... })(); // 常见写法
(function() { ... }()); // 也可以
!function() { ... }(); // 也可以(少见写法)
+function() { ... }(); // 有效(但不推荐)
应用:
- 封装变量,避免污染全局
var x = 100;
(function () {
var x = 10;
console.log("内部x:", x); // 10
})();
console.log("全局x:", x); // 100
- 初始化代码执行一次
const config = (function () {
const apiKey = "abc123";
const env = "production";
return { apiKey, env };
})();
- 在循环中“锁定”变量值(使用 var)
for (var i = 0; i < 3; i++) {
(function (j) {
setTimeout(() => {
console.log(j); // 0, 1, 2
}, j * 1000);
})(i);
}
函数的参数
1. 默认参数
function greet(name = "Guest") {
return `Hello, ${name}`;
}
2. 剩余参数(Rest Parameters)
function sum(...numbers) {
return numbers.reduce((a, b) => a + b);
}
3. arguments 对象(仅普通函数)
function showArgs() {
console.log(arguments);
}
函数特点
函数可以作为值赋给变量:
javascriptconst sayHi = function() {};
函数可以作为参数传递:
javascriptfunction callLater(callback) { callback(); }
函数可以作为返回值返回:
javascriptfunction multiplier(x) { return function(y) { return x * y; }; }
作用域链
- 什么是作用域
**作用域(Scope)**是变量的可访问范围。JavaScript
有以下几种作用域:
全局作用域:在所有函数外定义的变量(包括未声明直接赋值的变量);
函数作用域:函数内部定义的变量只能在函数内访问;
块级作用域(ES6):使用 let / const 在 {} 中定义的变量仅在块内有效。
什么是作用域链?
当访问变量时,JavaScript
会从当前作用域开始查找,若找不到,就沿着父作用域一层层向上查找,直到全局作用域为止。这个查找路径形成了作用域链。
作用域链是词法静态的,取决于函数定义位置,而非调用位置。
示例:
var a = 10;
function outer() {
var b = 20;
function inner() {
var c = 30;
console.log(a, b, c);
}
inner();
}
outer(); // 输出:10 20 30
//查找变量 a:在 inner 中找不到,去 outer,再去全局。[inner → outer → global]
闭包
闭包是指一个函数能够访问其词法作用域中定义的变量,即使这个函数是在其作用域外被调用的。
换句话说:
函数 + 它所捕获的作用域变量 = 闭包
function outer() {
let x = 10;
return function inner() {
console.log(x); // 闭包访问外层作用域
};
}
创建闭包的常见方式
- 返回函数
function outer() {
var count = 0;
return function () {
count++;
console.log(count);
};
}
const counter = outer();
counter(); // 1
counter(); // 2
- 在异步中使用
function greet(name) {
setTimeout(function () {
console.log("Hello, " + name);
}, 1000);
}
greet("Alice");
- 使用闭包实现私有变量
js中没有访问修饰符。
function createCounter() {
let count = 0; // 私有变量
return {
increment() {
count++;
console.log(count);
},
decrement() {
count--;
console.log(count);
}
};
}
const counter = createCounter();
counter.increment(); // 输出:1
counter.increment(); // 输出:2
counter.decrement(); // 输出:1
注意
- 闭包会导致变量不会被释放,可能导致内存泄露,慎重使用
- 每个闭包都会创建新的作用域链 ,大量使用会带来性能问题
- 闭包和
this
无直接关系,this
是运行时绑定,与闭包无关
对象
在 JavaScript 中,**对象(Object)是最核心的数据类型之一,用于存储键值对(key-value)**的数据结构。
什么是对象?
对象是一个由属性和方法组成的无序集合。
const person = {
name: "Alice",
age: 30,
greet: function () {
console.log("Hello, I'm " + this.name);
}
};
name
和age
是 属性(property)greet()
是 方法(method)this
指向当前对象
对象的创建方式
1. 对象字面量(最常用)
const obj = { a: 1, b: 2 };
2. 使用 new Object()
const obj = new Object();
obj.a = 1;
3. 使用构造函数
function Person(name) {
this.name = name;
}
const p = new Person("Tom");
4. 使用 Object.create(proto)
const parent = { greet() { console.log("Hi"); } };
const child = Object.create(parent);
child.name = "Child";
5. 使用类(ES6)
class Animal {
constructor(type) {
this.type = type;
}
}
const dog = new Animal("dog");
属性访问与操作
const obj = { name: "Bob", age: 25 };
// 点语法
console.log(obj.name); // "Bob"
// 中括号语法(适合变量动态访问)
console.log(obj["age"]); // 25
// 添加属性
obj.gender = "male";
// 删除属性
delete obj.age;
遍历对象属性
1. for...in
(可枚举自身+继承属性)
for (let key in obj) {
console.log(key, obj[key]);
}
2. Object.keys()
/ Object.values()
/ Object.entries()
Object.keys(obj); // ['name', 'gender']
Object.values(obj); // ['Bob', 'male']
Object.entries(obj); // [['name', 'Bob'], ['gender', 'male']]
对象的特性
- 对象是引用类型
当将一个引用类型的变量赋值给另一个变量时,复制的是引用地址,而不是对象本身。
const a = { value: 10 };
const b = a;
b.value = 20;
console.log(a.value); // 20(引用指向同一个对象)
- 对象比较的行为
在比较对象时,JavaScript 比较的是引用地址,而不是对象的内容。
let objA = { value: 10 };
let objB = { value: 10 };
console.log(objA === objB); // 输出: false
- 对象的动态性与可变性
引用类型的对象是动态的,可以随时添加、修改或删除其属性。
let person = {};
person.name = "John"; // 添加属性
person.age = 30;
delete person.age; // 删除属性
console.log(person); // 输出: { name: "John" }
- 深拷贝与浅拷贝
由于对象赋值是引用复制,多个变量可能指向同一个对象,导致修改一个变量会影响到其他变量。
为了避免这种情况,可以使用深拷贝创建对象的独立副本。
//浅拷贝
const original = { a: 1, b: { c: 2 } };
const shallowCopy = { ...original };
shallowCopy.b.c = 3;
console.log(original.b.c); // 输出: 3
// shallowCopy 是 original 的浅拷贝,修改嵌套对象 b.c 的值会影响到原对象。
//深拷贝
//1. JSON.parse(JSON.stringify(obj))
//优点:简单易用,适用于纯数据对象。
//缺点:无法处理函数、undefined、Symbol、循环引用、Date、RegExp、Map、Set 等特殊对象。
const original = { name: "Alice", details: { age: 25 } };
const copy = JSON.parse(JSON.stringify(original));
copy.details.age = 30;
console.log(original.details.age); // 输出: 25
//2. structuredClone(obj)
//优点:原生支持,能处理大多数数据类型,包括 Date、RegExp、Map、Set 等。
//缺点:不支持函数和某些特殊对象;在某些环境中可能不兼容。
const original = { a: 1, b: { c: 2 } };
const deepCopy = structuredClone(original);
deepCopy.b.c = 3;
console.log(original.b.c); // 输出: 2
//3. 第三方库(如 Lodash 的 cloneDeep)
const _ = require('lodash');
const original = { a: 1, b: { c: 2 } };
const deepCopy = _.cloneDeep(original);
deepCopy.b.c = 3;
console.log(original.b.c); // 输出: 2
- 垃圾回收机制
JavaScript 的垃圾回收器会自动释放不再被引用的对象所占用的内存。当一个对象没有任何引用指向它时,它就会被视为不可访问,进而被垃圾回收器回收。
let obj = { name: "Alice" };
obj = null; // 原对象不再被引用,等待垃圾回收
对象的常用方法
Object.assign()
用于将一个或多个源对象的属性复制到目标对象中,常用于对象的合并或浅拷贝。
const target = { a: 1 };
const source = { b: 2 };
const result = Object.assign(target, source);
console.log(result); // 输出: { a: 1, b: 2 }
Object.create()
创建一个新对象,使用指定的原型对象和可选的属性。
const proto = { greet() { console.log("Hello"); } };
const obj = Object.create(proto);
obj.greet(); // 输出: Hello
Object.keys()
返回一个数组,包含对象自身的所有可枚举属性的键名。
const obj = { a: 1, b: 2 };
console.log(Object.keys(obj)); // 输出: ['a', 'b']
Object.values()
返回一个数组,包含对象自身的所有可枚举属性的键值。
const obj = { a: 1, b: 2 };
console.log(Object.values(obj)); // 输出: [1, 2]
Object.entries()
返回一个数组,包含对象自身的所有可枚举属性的键值对数组。
const obj = { a: 1, b: 2 };
console.log(Object.entries(obj)); // 输出: [['a', 1], ['b', 2]]
Object.fromEntries()
将键值对数组转换为对象,常与 Object.entries()
搭配使用。
const entries = [['a', 1], ['b', 2]];
const obj = Object.fromEntries(entries);
console.log(obj); // 输出: { a: 1, b: 2 }
Object.freeze()
冻结一个对象,防止对其进行修改(添加、删除或更改属性)。
const obj = { a: 1 };
Object.freeze(obj);
obj.a = 2;
console.log(obj.a); // 输出: 1
Object.seal()
密封一个对象,防止添加或删除属性,但可以修改已有属性的值。
const obj = { a: 1 };
Object.seal(obj);
obj.a = 2;
console.log(obj.a); // 输出: 2
Object.hasOwnProperty()
判断对象是否具有指定的自身属性。
const obj = { a: 1 };
console.log(obj.hasOwnProperty('a')); // 输出: true
Object.getPrototypeOf()
和Object.setPrototypeOf()
获取或设置对象的原型。
const proto = {};
const obj = Object.create(proto);
console.log(Object.getPrototypeOf(obj) === proto); // 输出: true
原型与原型链
在 JavaScript 中,**原型(Prototype)和原型链(Prototype Chain)**是理解对象继承和属性查找机制的关键概念。它们构成了 JavaScript 的核心特性之一,尤其在实现继承和共享属性方面起着重要作用。
什么是原型(Prototype)
在 JavaScript 中,每个对象(除了 null
)在创建时都会与另一个对象关联,这个对象就是其原型。原型本身也是一个对象,因此也有自己的原型,形成一个链式结构,直到某个对象的原型为 null
为止,这个链式结构被称为原型链。
每个函数都有一个特殊的属性 prototype
,它指向一个对象,称为该函数的原型对象。当使用构造函数创建新对象时,新对象的内部属性 [[Prototype]]
会被设置为构造函数的 prototype
属性。
例如:
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
const alice = new Person('Alice');
alice.sayHello(); // 输出: Hello, I'm Alice
在这个例子中,alice
对象的原型是 Person.prototype
,因此可以访问 sayHello
方法。
什么是原型链(Prototype Chain)
原型链是由多个对象通过其原型连接形成的链式结构。当访问一个对象的属性或方法时,JavaScript 引擎会首先在该对象自身查找,如果找不到,则会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的末尾(即原型为 null
的对象)。([CSDN博客][3], [CSDN博客][1])
例如:
console.log(alice.toString()); // 输出: [object Object]
虽然
alice
对象没有定义toString
方法,但它的原型链上有Object.prototype
,而Object.prototype
定义了toString
方法,因此可以调用。
原型链的结构如下:
// 每个箭头表示对象的原型指向。
alice --> Person.prototype --> Object.prototype --> null
原型和原型链的作用
- 实现继承:通过原型链,JavaScript 对象可以继承其他对象的属性和方法,实现代码复用。
- 属性查找机制:当访问对象的属性或方法时,JavaScript 会沿着原型链查找,直到找到为止。这种机制使得对象可以共享属性和方法。
- 节省内存:将共享的属性和方法定义在原型上,可以避免每个实例都创建一份,节省内存空间。
注意事项
- 原型链的终点是
null
:原型链最终会指向null
,表示没有更多的原型可查找。 - 避免过深的原型链:过深的原型链会增加属性查找的时间,影响性能。
- 修改原型会影响所有实例:如果修改了构造函数的原型对象,所有通过该构造函数创建的实例都会受到影响。
原型模式
在 JavaScript
中,**原型模式(Prototype Pattern)**是一种创建型设计模式,它通过复制(克隆)已有对象来创建新对象,而不是通过实例化类。 这种模式利用了 JavaScript
的原型机制,使得对象之间可以共享属性和方法,从而提高代码的复用性和效率。
原型模式的核心思想是:
使用一个已经创建的对象作为原型,通过复制该原型对象来创建一个与原型相同或相似的新对象。
在 JavaScript
中,由于其基于原型的特性,每个对象都可以作为其他对象的原型。这使得原型模式在 JavaScript 中具有天然的优势。
如何实现原型模式
- 使用
Object.create()
- 使用
Object.create()
方法创建一个新对象,使用指定的原型对象和可选的属性。
const prototypeObject = {
greet() {
console.log(`Hello, I'm ${this.name}`);
}
};
const newObj = Object.create(prototypeObject);
newObj.name = 'Alice';
newObj.greet(); // 输出: Hello, I'm Alice
在这个例子中,newObj
继承了 prototypeObject
的方法 greet
,实现了对象之间的属性和方法共享。([阿里云开发者社区][4])
- 使用构造函数和原型
通过构造函数定义对象的属性,并将方法添加到构造函数的原型上,实现方法共享。([CSDN博客][5])
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log(`Hello, I'm ${this.name}`);
};
const alice = new Person('Alice');
alice.greet(); // 输出: Hello, I'm Alice
在上述示例中,所有通过 Person
构造函数创建的实例都共享 greet
方法,节省了内存。
在 JavaScript 中,**原型模式(Prototype Pattern)**是一种创建型设计模式,它通过克隆已有对象来创建新对象,而不是通过实例化类。这种模式充分利用了 JavaScript 的原型机制,使得对象之间可以共享属性和方法,从而提高代码的复用性和效率。
原型模式的常见应用场景
- 对象模板与克隆
当需要创建多个结构相似但数据不同的对象时,可以先定义一个原型对象,然后通过克隆该原型对象来创建新对象。
const employeePrototype = {
greet() {
console.log(`Hello, I'm ${this.name}`);
}
};
const employee1 = Object.create(employeePrototype);
employee1.name = 'Alice';
employee1.greet(); // 输出: Hello, I'm Alice
在上述示例中,employee1
继承了 employeePrototype
的方法 greet
,实现了对象之间的属性和方法共享。
- 性能优化
对于需要进行复杂初始化的对象,可以先创建一个原型对象,然后通过克隆该原型对象来创建新对象,避免重复的初始化过程,从而提高性能。
function HeavyObject() {
this.data = /* 复杂的初始化过程 */;
}
HeavyObject.prototype.clone = function() {
const clone = Object.create(this);
// 如果需要,复制或重置特定属性
return clone;
};
const prototypeObj = new HeavyObject();
const newObj = prototypeObj.clone();
通过这种方式,可以避免每次都进行复杂的初始化过程,提高对象创建的效率。
- UI 组件复用
在前端开发中,常常需要创建多个相似的 UI 组件。可以先定义一个组件的原型对象,然后通过克隆该原型对象来创建新组件,实现组件的复用。
const buttonPrototype = {
type: 'button',
render() {
console.log(`Rendering a ${this.type} with label: ${this.label}`);
}
};
const submitButton = Object.create(buttonPrototype);
submitButton.label = 'Submit';
submitButton.render(); // 输出: Rendering a button with label: Submit
通过这种方式,可以快速创建多个相似的组件,提高开发效率。
工厂函数
在 JavaScript 中,**工厂函数(Factory Function)**是一种创建对象的函数,它不使用 class
或 constructor
,而是通过返回一个新对象来实现实例化。
工厂函数提供了一种灵活的方式来生成对象,尤其适用于需要封装私有数据或避免使用 this
和 new
的场景。
什么是工厂函数?
工厂函数是一个普通的函数,它返回一个新对象。与构造函数不同,工厂函数不需要使用 new
关键字,也不依赖于 this
。这使得工厂函数在某些情况下更容易理解和使用。
示例:
function createPerson(name, age) {
return {
name,
age,
greet() {
console.log(`Hello, I'm ${this.name}`);
}
};
}
const person1 = createPerson('Alice', 30);
person1.greet(); // 输出: Hello, I'm Alice
工厂函数与闭包
工厂函数可以利用闭包来创建私有变量,从而实现数据的封装。
示例:
function createCounter() {
let count = 0;
return {
increment() {
count++;
console.log(count);
},
decrement() {
count--;
console.log(count);
}
};
}
const counter = createCounter();
counter.increment(); // 输出: 1
counter.increment(); // 输出: 2
counter.decrement(); // 输出: 1
工厂函数 vs 构造函数 vs 类
特性 | 工厂函数 | 构造函数 | 类(ES6) |
---|---|---|---|
使用 new | 否 | 是 | 是 |
使用 this | 否 | 是 | 是 |
私有变量支持 | 是(通过闭包) | 否(需额外手段) | 是(通过私有字段) |
原型方法共享 | 否(每个实例独立) | 是 | 是 |
可读性与简洁性 | 高 | 中 | 高 |
适用场景 | 简单对象、私有数据封装 | 原型继承、性能优化 | 面向对象编程、继承关系 |
工厂函数适合用于创建简单对象或需要封装私有数据的场景;构造函数和类则更适合需要原型继承和性能优化的复杂对象结构。
工厂函数的优点
- 避免使用
new
和this
:减少了上下文绑定错误的可能性。 - 支持私有数据:通过闭包实现数据封装。
- 灵活性高:可以根据需要返回不同结构的对象。
- 易于测试和维护:函数式风格使得代码更易于理解和测试。
代理
在 JavaScript 中,代理(Proxy) 是一种用于定义对象基本操作行为的机制,比如属性访问、赋值、函数调用等。它允许你拦截和自定义这些行为。
基本语法
const proxy = new Proxy(target, handler);
target
: 被代理的对象。handler
: 一个对象,包含拦截操作的函数(称为“陷阱”trap)。
示例:拦截属性读取
const person = {
name: 'Alice',
age: 30
};
const proxy = new Proxy(person, {
get(target, property) {
console.log(`读取属性:${property}`);
return target[property];
}
});
console.log(proxy.name); // 控制台输出:读取属性:name,然后输出 Alice
常用的拦截器(Handler 方法)
拦截器(Trap) | 用途说明 |
---|---|
get | 读取属性时触发 |
set | 修改属性值时触发 |
has | 使用 in 操作符时触发 |
deleteProperty | 使用 delete 删除属性时触发 |
ownKeys | 使用 Object.keys() 或 for...in 时触发 |
apply | 函数被调用时触发(函数代理) |
construct | 使用 new 调用构造函数时触发 |
示例:限制属性写入
const proxy = new Proxy(person, {
set(target, property, value) {
if (property === 'age' && typeof value !== 'number') {
throw new TypeError('年龄必须是数字');
}
target[property] = value;
return true;
}
});
proxy.age = 35; // ✅ 正常赋值
proxy.age = 'thirty'; // ❌ 抛出 TypeError
函数代理示例
function sum(a, b) {
return a + b;
}
const proxyFunc = new Proxy(sum, {
apply(target, thisArg, args) {
console.log(`调用函数:${args}`);
return target(...args);
}
});
proxyFunc(3, 4); // 输出:调用函数:3,4,返回 7
应用场景
- 数据验证(如 Vue 的响应式系统)
- 自动填充默认值
- 访问日志记录
- 防止非法访问
- 构建动态 API
示例一:响应式数据绑定(类似 Vue 2/3)
使用 Proxy
可以追踪对象属性的“读取”和“修改”,从而实现响应式数据绑定。
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
console.log(`读取属性:${key}`);
return Reflect.get(target, key);
},
set(target, key, value) {
console.log(`设置属性:${key} = ${value}`);
const result = Reflect.set(target, key, value);
// 通常这里可以触发更新 DOM
return result;
}
});
}
// 使用
const state = reactive({ count: 0 });
console.log(state.count); // 打印:读取属性:count,返回 0
state.count = 1; // 打印:设置属性:count = 1
在 Vue 3 中就是用
Proxy
实现的响应式数据系统,而 Vue 2 用的是Object.defineProperty
。
示例二:API 包装器(拦截 API 请求或统一处理)
通过 Proxy
,可以封装一个 API 对象,自动处理路径拼接、鉴权、日志、错误处理等。
const apiBase = 'https://api.example.com/';
const api = new Proxy({}, {
get(target, prop) {
return async function(params) {
const url = `${apiBase}${prop}`;
console.log(`请求:${url},参数:`, params);
try {
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(params),
headers: { 'Content-Type': 'application/json' }
});
return await response.json();
} catch (err) {
console.error(`调用 ${prop} 失败`, err);
throw err;
}
};
}
});
// 使用示例
api.login({ username: 'user', password: '123456' });
// 自动发起 POST 请求到 https://api.example.com/login
优点:
- 接口统一:所有接口都使用相同的写法,比如
api.xxx(params)
。 - 自动拼接 URL。
- 可以集中处理异常、token、缓存等逻辑。
this
在 JavaScript 中,this
是一个关键字,表示函数执行时的上下文对象。它的值并不是在函数定义时确定的,而是在函数调用时根据调用方式动态绑定的。
JavaScript 中的 this
主要有以下几种绑定规则,按照优先级从低到高排列:
1. 默认绑定(全局或普通函数调用)
在非严格模式下,独立调用的函数中的 this
指向全局对象(浏览器中为 window
,Node.js 中为 global
)。在严格模式下,this
为 undefined
。
function show() {
console.log(this);
}
show(); // 非严格模式下输出: Window; 严格模式下输出: undefined
2. 隐式绑定(对象方法调用)
当函数作为对象的方法被调用时,this
指向该对象。
const obj = {
name: 'Alice',
greet() {
console.log(this.name);
}
};
obj.greet(); // 输出: Alice
需要注意的是,如果将方法赋值给另一个变量再调用,this
会丢失原有的绑定。
const greetFunc = obj.greet;
greetFunc(); // 非严格模式下输出: undefined 或 window.name 的值
3. 显式绑定(call
、apply
、bind
)
使用 call
或 apply
方法可以显式地指定函数执行时的 this
。bind
方法则返回一个新的函数,永久绑定指定的 this
。
function greet() {
console.log(this.name);
}
const person = { name: 'Bob' };
greet.call(person); // 输出: Bob
greet.apply(person); // 输出: Bob
const boundGreet = greet.bind(person);
boundGreet(); // 输出: Bob
4. 构造函数绑定(new
关键字)
当使用 new
关键字调用函数时,this
指向新创建的对象。
function Person(name) {
this.name = name;
}
const alice = new Person('Alice');
console.log(alice.name); // 输出: Alice
即使函数内部使用了 call
或 apply
改变 this
,在使用 new
调用时,this
仍指向新创建的对象。
5. 箭头函数绑定(词法作用域)
箭头函数没有自己的 this
,它的 this
由外层作用域决定。这使得箭头函数在某些情况下非常有用,例如在回调函数中保持 this
的指向。
const obj = {
name: 'Charlie',
greet: () => {
console.log(this.name);
}
};
obj.greet(); // 输出: undefined(或 window.name 的值)
在上述示例中,箭头函数的 this
并不指向 obj
,而是继承自外层作用域的 this
。
优先级
当多个规则同时适用时,this
的指向遵循以下优先级(从高到低):
new
绑定- 显式绑定(
call
、apply
、bind
) - 隐式绑定
- 默认绑定(全局或普通函数调用)
需要注意的是,箭头函数的 this
是在定义时确定的,不受上述规则影响。
注意事项
事件处理函数中的
this
:在 DOM 事件处理函数中,this
默认指向触发事件的元素。javascriptdocument.getElementById('btn').addEventListener('click', function() { console.log(this); // 输出: 触发事件的按钮元素 });
定时器函数中的
this
:在setTimeout
或setInterval
的回调函数中,this
默认指向全局对象。javascriptsetTimeout(function() { console.log(this); // 非严格模式下输出: Window; 严格模式下输出: undefined }, 1000);
可以使用箭头函数或 bind
方法来保持期望的 this
指向。
setTimeout(() => {
console.log(this); // 输出: 外层作用域的 this
}, 1000);
方法丢失
this
绑定:当从对象中提取方法并单独调用时,this
的绑定会丢失。javascriptconst obj = { name: 'Dave', greet() { console.log(this.name); } }; const greetFunc = obj.greet; greetFunc(); // 输出: undefined(或 window.name 的值)
可以使用 bind
方法固定 this
的指向。
const boundGreet = obj.greet.bind(obj);
boundGreet(); // 输出: Dave
数据结构
Array(数组)
数组用于存储有序元素集合,可以是任何类型。
const fruits = ["apple", "banana", "cherry"];
// 添加元素
fruits.push("date"); // 添加到末尾
fruits.unshift("avocado"); // 添加到开头
// 删除元素
fruits.pop(); // 删除末尾
fruits.shift(); // 删除开头
// 查找元素
fruits.indexOf("banana"); // 1
fruits.includes("cherry"); // true
// 遍历
fruits.forEach(fruit => console.log(fruit));
// 转换
const upperFruits = fruits.map(fruit => fruit.toUpperCase());
// 过滤
const longFruits = fruits.filter(fruit => fruit.length > 5);
// 归并
const all = fruits.reduce((acc, fruit) => acc + " " + fruit);
// 排序
fruits.sort();
fruits.reverse();
//splice
// start:开始修改的位置索引。
// deleteCount:要删除的元素个数。
// item1, item2, ...:可选,要插入到数组中的元素。
// splice() 会改变原数组,并返回被删除的元素数组。
// array.splice(start[, deleteCount[, item1, item2, ...]])
let arr = [1, 2, 3, 4, 5];
let removed = arr.splice(2, 2); // 从索引2开始,删除2个元素
console.log(arr); // [1, 2, 5]
console.log(removed); // [3, 4]
let arr1 = ['a', 'b', 'e', 'f'];
arr.splice(2, 0, 'c', 'c1', 'd');// 输出:['a', 'b', 'c', 'c1', 'd', 'e', 'f']
//清空数组
//手动修改 length 属性为更小的值时,JS 引擎会自动移除超出新长度范围的所有元素。
let arr = [1, 2, 3];
arr.length = 0;
console.log(arr); // []
对象数组
// 对象数组
const users = [
{ id: 1, name: 'Alice', age: 28 },
{ id: 2, name: 'Bob', age: 34 },
{ id: 3, name: 'Charlie', age: 22 },
{ id: 4, name: 'David', age: 34 }
];
// 1. 查找:年龄为 34 的第一个用户
const firstAge34 = users.find(user => user.age === 34);
console.log('First user with age 34:', firstAge34);
// 2. 过滤:年龄大于 25 的所有用户
const olderThan25 = users.filter(user => user.age > 25);
console.log('Users older than 25:', olderThan25);
// 3. 映射:提取所有用户名
const names = users.map(user => user.name);
console.log('All user names:', names);
// 4. 排序:按年龄升序排列(不修改原数组)
const sortedByAge = [...users].sort((a, b) => a.age - b.age);
console.log('Users sorted by age:', sortedByAge);
// 5. 累加:计算所有用户年龄总和
const totalAge = users.reduce((sum, user) => sum + user.age, 0);
console.log('Total age of all users:', totalAge);
// 6. 组合操作:找出年龄 > 30 的用户名
const namesOver30 = users
.filter(user => user.age > 30)
.map(user => user.name);
console.log('Names of users over 30:', namesOver30);
Set(集合)
Set
是一种不允许重复元素的数据结构。
const ids = new Set([1, 2, 3, 2, 1]);
ids.add(4); // 添加
ids.delete(2); // 删除
ids.has(3); // 是否存在某值
ids.size; // 长度
// 遍历
for (let id of ids) {
console.log(id);
}
注意
Set 不能直接去重“值相同”的对象,因为对象比较的是引用
// 创建一个对象数组
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
];
// Set 不能直接去重“值相同”的对象,因为对象比较的是引用
const userSet = new Set(users);
console.log('Original Set size:', userSet.size); // 3
// 尝试加入重复对象(值相同,但不是同一引用)
userSet.add({ id: 1, name: 'Alice' });
console.log('Set size after adding same value object:', userSet.size); // 4,因为是不同引用
// 如果你想基于 id 去重,可以手动处理
const moreUsers = [
{ id: 2, name: 'Bob' }, // duplicate id
{ id: 4, name: 'David' }
];
// 将原数组与新数组合并,并根据 id 去重
const merged = [...users, ...moreUsers];
const uniqueById = Array.from(
new Map(merged.map(user => [user.id, user])).values()
);
console.log('Unique users by ID:', uniqueById);
Map(映射)
Map
是键值对集合,键可以是任意类型(包括对象)。
const userMap = new Map();
userMap.set("name", "Alice");
userMap.set("age", 25);
console.log(userMap.get("name")); // "Alice"
console.log(userMap.has("age")); // true
userMap.delete("age");
userMap.size; // 1
// 遍历
for (const [key, value] of userMap) {
console.log(key, value);
}
对象Map
// 创建一个 Map
const userMap = new Map();
// 添加元素:以用户 ID 为 key,用户对象为 value
userMap.set(1, { id: 1, name: 'Alice' });
userMap.set(2, { id: 2, name: 'Bob' });
// 读取某个用户
const user1 = userMap.get(1);
console.log('User with ID 1:', user1); // { id: 1, name: 'Alice' }
// 判断是否存在某个 key
console.log('Has ID 2?', userMap.has(2)); // true
// 更新用户
userMap.set(2, { id: 2, name: 'Robert' });
// 删除用户
userMap.delete(1);
// Map 的大小
console.log('Map size:', userMap.size); // 1
// 遍历 Map
for (const [id, user] of userMap) {
console.log(`User ID: ${id}, Name: ${user.name}`);
}
map
和 object
特性 | Object | Map |
---|---|---|
键类型 | 仅字符串或符号 | 任意类型 |
键的有序性 | 无 | 有 |
迭代性 | 不可直接迭代(for...in 替代) | 可直接 for..of |
性能 | 较慢(键多时) | 较快 |
特性 | for...in | for...of |
---|---|---|
可用于对象吗? | 是设计来遍历对象的 | 普通对象不能直接用 |
遍历的内容 | 对象的键名(包括继承的) | 可迭代对象的元素值 |
是否会遍历原型链? | 会 | 不会 |
常用于 | 遍历普通对象 | 遍历数组、Map、Set、字符串等 |
WeakMap 和 WeakSet
它们是 Map 和 Set 的变体,但只能使用对象作为键(WeakMap)或值(WeakSet),并且是弱引用,不影响垃圾回收。
适合缓存或私有属性存储,但不能遍历。
let obj = { id: 1 };
const weakMap = new WeakMap();
weakMap.set(obj, "some value");
TypedArray(类型数组)
用于处理二进制数据,如 Int8Array
, Uint8Array
, Float32Array
等,适用于性能要求较高的应用。
const buffer = new ArrayBuffer(8); // 8 字节的内存
const int32 = new Int32Array(buffer);
int32[0] = 42;
console.log(int32[0]); // 42
数据结构 | 是否有序 | 是否唯一 | 键类型 | 常见用途 |
---|---|---|---|---|
Array | 是 | 否 | 数字索引 | 有序元素集合 |
Set | 是 | 是 | 值本身 | 唯一集合 |
Map | 是 | 是 | 任意类型 | 更强大的键值存储 |
WeakMap | 不可遍历 | 是 | 对象 | 私有数据、缓存 |
WeakSet | 不可遍历 | 是 | 对象(值) | 追踪对象存在性 |
TypedArray | 是 | 否 | 数字索引 | 高性能数值处理 |
事件
JavaScript 的事件(Event)是浏览器与用户交互的重要机制,它允许我们对用户的各种行为(点击、输入、键盘、加载等)做出响应。
以下是 JS 事件系统的详细介绍,包括事件的基本概念、类型、事件模型、冒泡与捕获、事件对象、事件委托、移除监听器等方面的知识,配合示例说明。
什么是事件?
事件是用户或浏览器执行的某种行为,如:
- 用户点击按钮(
click
) - 鼠标移入元素(
mouseover
) - 输入框内容改变(
input
) - 页面加载完成(
load
) - 键盘按下(
keydown
)
事件监听方式
- 通过 HTML 属性绑定(不推荐)
<button onclick="alert('Clicked')">Click me</button>
- 使用 DOM 元素的事件属性
const btn = document.querySelector("button");
btn.onclick = function () {
alert("Clicked");
};
addEventListener
(推荐)
btn.addEventListener("click", () => {
alert("Clicked via addEventListener");
});
可以添加多个事件处理器,并支持事件捕获阶段。
事件对象 event
每个事件处理器会自动接收一个
event
参数,包含事件的所有信息。
btn.addEventListener("click", function (event) {
console.log(event.type); // "click"
console.log(event.target); // 触发事件的元素
console.log(event.currentTarget); // 绑定事件的元素
});
常见属性包括:
属性 | 说明 |
---|---|
type | 事件类型,例如 "click" |
target | 实际触发事件的元素 |
currentTarget | 当前绑定事件的元素 |
preventDefault() | 阻止默认行为(如表单提交) |
stopPropagation() | 阻止事件冒泡 |
事件传播机制:捕获、目标、冒泡
事件在 DOM 树上会经历三个阶段:
- 捕获阶段(Capture Phase)
- 目标阶段(Target Phase)
- 冒泡阶段(Bubble Phase)
parent.addEventListener("click", () => {
console.log("Parent capture");
}, true); // 捕获阶段
parent.addEventListener("click", () => {
console.log("Parent bubble");
}, false); // 冒泡阶段
默认是冒泡。设置
useCapture=true
监听捕获阶段。
事件委托(Event Delegation)
事件委托是将子元素的事件绑定到父元素上,利用事件冒泡提高性能。
<ul id="list">
<li>Item 1</li>
<li>Item 2</li>
</ul>
document.getElementById("list").addEventListener("click", function (e) {
if (e.target.tagName === "LI") {
console.log("Clicked:", e.target.textContent);
}
});
优点:
- 子元素可以动态添加
- 节省内存,减少事件绑定
事件解绑
使用 removeEventListener
:
function handleClick() {
alert("Click");
}
btn.addEventListener("click", handleClick);
btn.removeEventListener("click", handleClick); // 必须是同一个函数引用
常见事件类型
类型 | 示例事件 | 用途 |
---|---|---|
鼠标事件 | click , dblclick , mousemove , mouseover , mouseout | 交互响应、UI动态 |
键盘事件 | keydown , keyup , keypress | 表单输入、快捷键 |
表单事件 | submit , change , input , focus , blur | 表单验证与交互 |
文档事件 | DOMContentLoaded , load , resize , scroll | 页面加载控制、UI响应 |
拖拽事件 | drag , dragstart , drop , dragover | 文件拖放、组件拖拽 |
触摸事件 | touchstart , touchmove , touchend | 移动端操作 |
自定义事件(CustomEvent)
你可以创建并触发自定义事件:
const myEvent = new CustomEvent("my-event", {
detail: { message: "Hello" }
});
element.addEventListener("my-event", e => {
console.log(e.detail.message); // "Hello"
});
element.dispatchEvent(myEvent);
防抖 & 节流(与事件结合使用)
- 防抖(debounce):在事件触发n 秒后只执行一次,适用于输入框等场景
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
input.addEventListener("input", debounce(() => {
console.log("Search...");
}, 300));
- 节流(throttle):每隔 n 秒执行一次,适用于 scroll、resize 等高频事件
常见实现方式:
方法一:时间戳版节流(立即执行,固定间隔)
function throttle(fn, delay) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= delay) {
fn.apply(this, args);
lastTime = now;
}
};
}
📦 用法示例:
window.addEventListener('resize', throttle(() => {
console.log('窗口变化了', new Date());
}, 500));
方法二:定时器版节流(延迟执行)
function throttle(fn, delay) {
let timer = null;
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
}
};
}
区别:
特性 | 时间戳版 | 定时器版 |
---|---|---|
是否立即执行 | 是 | 否(延迟) |
是否停止后最后一次执行 | 否 | 是 |
方法三:时间戳 + 定时器结合版(兼顾首次立即执行和停止后补执行)
function throttle(fn, delay) {
let lastTime = 0;
let timer = null;
return function (...args) {
const now = Date.now();
const remaining = delay - (now - lastTime);
if (remaining <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(this, args);
lastTime = now;
} else if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
lastTime = Date.now();
timer = null;
}, remaining);
}
};
}
异步编程
JavaScript 中的 异步编程(Asynchronous Programming) 是一种允许程序在不阻塞主线程的情况下执行耗时操作(如网络请求、定时器、文件读写等)的编程方式。
由于 JavaScript 是单线程运行的,异步机制对保持程序流畅性至关重要。
如果所有任务都是同步的,那么一个耗时操作(如访问服务器)会阻塞整个页面,导致“卡死”。
异步编程的目的是:
- 避免主线程阻塞
- 提高程序响应性
- 实现任务并发或延迟执行
事件循环(Event Loop)机制
JavaScript 的异步编程执行过程,背后主要依赖于 事件循环(Event Loop)机制,它决定了异步任务何时执行。
整体执行流程
执行全局同步代码
- 所有同步任务按顺序进栈、出栈。
遇到异步任务
- 将异步任务交由 Web APIs 处理。
Web APIs 处理完成后
- 将回调函数加入任务队列中(宏任务或微任务)。
同步代码执行完毕,调用栈为空
事件循环开始:
- 先执行 微任务队列 中所有任务(如 Promise 的
.then()
) - 然后取出一个宏任务(如
setTimeout
),放入调用栈中执行
- 先执行 微任务队列 中所有任务(如 Promise 的
重复执行 4 步骤 —— 这就是事件循环(Event Loop)
示例:同步 + 异步代码混合执行
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
步骤 | 行为 | 输出 | 状态说明 |
---|---|---|---|
1 | 执行同步代码 console.log(1) | 1 | 立即执行 |
2 | 遇到 setTimeout | - | 注册回调函数,加入宏任务队列 |
3 | 执行 Promise.resolve().then | - | 回调注册进 微任务队列 |
4 | 执行同步代码 console.log(4) | 4 | 立即执行 |
5 | 主线程空闲,执行 微任务队列 | 3 | 执行 .then() 内容 |
6 | 微任务队列清空后执行宏任务 | 2 | 执行 setTimeout 的回调 |
最终输出顺序是:
1
4
3
2
宏任务 vs 微任务
- 宏任务
一类在事件循环中每次循环执行一次的任务。每执行一个宏任务,接着就会清空所有微任务。
setTimeout
,setInterval
,fetch
,DOM事件
等执行会将任务放入宏任务队列
- 微任务
在当前宏任务执行完毕后、下一个宏任务开始前立即执行的任务。执行优先级高于宏任务。
Promise.then/catch/finally
,queueMicrotask
,MutationObserver
等执行会将任务放入微任务队列
异步编程的几种常见方式
1. 回调函数(Callback)
最早期的异步处理方式。通过将函数作为参数传入另一个函数,在异步操作完成后调用。
function loadData(callback) {
setTimeout(() => {
console.log("数据加载完成");
callback("数据内容");
}, 1000);
}
loadData((data) => {
console.log("回调接收数据:", data);
});
🔴 缺点:回调地狱(Callback Hell)
step1(() => {
step2(() => {
step3(() => {
// ...
});
});
});
2. Promise
Promise
是对异步操作结果的抽象表示,它有三种状态:
pending
(进行中)fulfilled
(已成功)rejected
(已失败)
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) resolve("成功");
else reject("失败");
}, 1000);
});
promise
.then((result) => console.log("结果:", result))
.catch((error) => console.error("错误:", error));
🌟 优势:
- 链式调用:避免了回调地狱
- 错误统一处理(
.catch
)
3. async/await(语法糖)
async/await
是对 Promise 的封装,提供了 同步写法,异步执行 的语法体验,极大提升了代码可读性。
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function fetchData() {
console.log("开始加载");
await delay(1000); // 等待 1 秒
console.log("数据加载完成");
}
fetchData();
⚠️ 注意:
await
只能在async
函数内部使用- 如果
await
后面不是 Promise,会自动转成一个已完成的 Promise
4. 并发与串行控制
并发(并行执行多个任务):
async function parallel() {
const [res1, res2] = await Promise.all([
fetch("/api/data1"),
fetch("/api/data2")
]);
}
串行(一个一个来):
async function serial() {
const res1 = await fetch("/api/data1");
const res2 = await fetch("/api/data2");
}
5. 常见异步操作类型
setTimeout
,setInterval
fetch
,XMLHttpRequest
Event
事件监听- 浏览器 API(如 Geolocation、File API)
- Node.js 中的文件操作、网络通信
DOM
JavaScript 的 DOM(Document Object Model,文档对象模型) 是前端开发中最核心的概念之一,它让我们可以用 JavaScript 动态地访问和操作网页的内容、结构和样式。
什么是 DOM?
DOM 是一种 树状结构,它将 HTML 文档表示为由节点组成的层级结构,每个 HTML 标签、文本、属性等都是节点。
比如,下面的 HTML:
<!DOCTYPE html>
<html>
<head>
<title>页面标题</title>
</head>
<body>
<p>Hello World</p>
</body>
</html>
被浏览器解析成如下的 DOM 结构:
Document
└── html
├── head
│ └── title
└── body
└── p
DOM 中的节点类型
节点类型 | 示例 | 说明 |
---|---|---|
元素节点 | <div> , <p> | HTML 元素 |
文本节点 | "Hello" | 元素内的文字内容 |
属性节点 | class="box" | 元素的属性(较少直接使用) |
注释节点 | <!-- 注释 --> | HTML 注释 |
文档节点 | document | 整个文档对象 |
常用 DOM 操作
- 获取元素
document.getElementById('myId'); // 单个元素
document.getElementsByClassName('myClass'); // HTMLCollection
document.getElementsByTagName('div'); // HTMLCollection
document.querySelector('.myClass'); // 单个元素
document.querySelectorAll('div.myClass'); // NodeList
- 修改内容或属性
const el = document.getElementById('title');
el.textContent = '新标题';
el.innerHTML = '<span>带标签</span>';
el.setAttribute('class', 'new-class');
- 修改样式
el.style.color = 'red';
el.style.fontSize = '20px';
- 操作类名(推荐)
el.classList.add('active');
el.classList.remove('hidden');
el.classList.toggle('visible');
el.classList.contains('open'); // 判断是否包含某个类
- 创建与插入元素
const newEl = document.createElement('div');
newEl.textContent = '我是新元素';
document.body.appendChild(newEl); // 插入到 body 的末尾
- 删除元素
const el = document.getElementById('toDelete');
el.remove();
DOM 事件(交互)
const btn = document.querySelector('button');
btn.addEventListener('click', function (event) {
alert('按钮被点击了');
});
常见事件类型 | 描述 |
---|---|
click | 鼠标点击 |
input | 输入框内容变化 |
submit | 表单提交 |
keydown | 键盘按键按下 |
mouseover | 鼠标悬停 |
DOM 的特性
- DOM 操作是浏览器提供的接口,不是 JavaScript 本身的功能(JavaScript 是操作工具,DOM 是目标结构)
- DOM 操作相对“慢”,频繁改动会影响性能 → 建议使用 DocumentFragment 或 虚拟 DOM(如 Vue/React) 优化
BOM
JavaScript 中的 BOM(Browser Object Model,浏览器对象模型) 是浏览器提供的一组 API,用于与浏览器窗口进行交互,而不是与网页的内容(DOM)交互。
什么是 BOM?
BOM 提供了一些全局对象和方法,主要用于控制浏览器窗口、导航、弹窗、获取浏览器信息等。 它不是由 ECMAScript 标准定义的,而是由各大浏览器实现的“扩展”。
BOM 的核心对象结构图
window
├── location
├── navigator
├── history
├── screen
├── document (这是 DOM 对象)
├── alert(), confirm(), prompt()
└── setTimeout(), setInterval(), clearTimeout() 等
主要 BOM 组件
1. window
(顶层对象)
window.alert('Hello'); // 弹窗
alert('Hello'); // window 可省略
常用功能:
方法 | 描述 |
---|---|
alert() | 弹出提示框(只有确认) |
confirm() | 弹出确认框(返回 true/false) |
prompt() | 输入框(返回输入的字符串) |
setTimeout() | 延迟执行一次 |
setInterval() | 每隔一段时间重复执行 |
clearTimeout() | 清除延时 |
clearInterval() | 清除定时器 |
2. location
(地址栏信息)
console.log(location.href); // 当前完整 URL
location.href = 'https://www.example.com'; // 跳转
常用属性:
属性 | 描述 |
---|---|
href | 当前页面完整 URL |
hostname | 主机名(如 www.example.com ) |
pathname | 路径(如 /index.html ) |
search | 查询字符串(如 ?q=abc ) |
hash | 锚点部分(如 #top ) |
3. navigator
(浏览器信息)
console.log(navigator.userAgent); // 浏览器 UA 字符串
console.log(navigator.language); // 浏览器语言
常用于判断浏览器类型、平台、语言等(但不建议依赖 UA 判断特性)。
4. history
(浏览历史)
history.back(); // 返回上一页
history.forward(); // 前进一页
history.go(-1); // 回退 1 步
注意:出于安全考虑,不能访问历史记录的具体 URL 内容。
5. screen
(屏幕信息)
console.log(screen.width); // 屏幕宽度
console.log(screen.availHeight); // 可用高度
BOM 和 DOM 的区别
项目 | DOM(Document) | BOM(Browser) |
---|---|---|
对象 | document | window , location , navigator 等 |
作用 | 操作网页内容 | 操作浏览器行为 |
标准 | ECMAScript + W3C | 非官方标准(由浏览器实现) |
DOM 是网页的“内容”,BOM 是浏览器的“外壳”。
本地存储
在 Web 前端中,浏览器为我们提供了多种本地存储机制,用于在客户端保存数据。主要有以下几种方式:
类型 | 大小限制 | 生命周期 | 与服务器交互 | 示例用途 |
---|---|---|---|---|
Cookie | ~4KB | 可设定过期时间 | 每次请求自动发送 | 登录状态、跨页面识别 |
LocalStorage | ~5MB | 永久(除非主动清除) | 否 | 保存用户设置、主题色等 |
SessionStorage | ~5MB | 页面会话(关闭失效) | 否 | 多标签隔离、一次性表单数据 |
Cookie
- 每次 HTTP 请求都会自动携带 Cookie(影响性能和安全)
- 可以通过 JS 访问(
document.cookie
) - 支持设置
HttpOnly
、Secure
等属性提高安全性
document.cookie = "username=Tom; expires=Fri, 01 Jan 2027 12:00:00 UTC; path=/";
LocalStorage
- 每个域名下存储空间独立
- 永久保存(除非手动删除或清空缓存)
localStorage.setItem('theme', 'dark');
let value = localStorage.getItem('theme'); // "dark"
localStorage.removeItem('theme');
localStorage.clear(); // 清空所有
SessionStorage
- 和 LocalStorage 类似,但仅在当前标签页/窗口有效
- 页面关闭或刷新后数据丢失
sessionStorage.setItem('step', '1');
let step = sessionStorage.getItem('step');
使用建议
需求 | 推荐方式 |
---|---|
跨页面登录状态、CSRF 等 | Cookie (带 HttpOnly) |
用户设置、持久数据 | LocalStorage |
页面临时数据,如表单暂存 | SessionStorage |
网络请求
在 JavaScript 中,网络请求是前端与后端通信的核心方式。开发者可以使用原生 API 或第三方库来实现 HTTP 请求。以下是常用的网络请求方式及其实现方式的详细介绍:
原生网络请求方式
1. XMLHttpRequest(XHR)
XMLHttpRequest
是早期实现 AJAX 的核心 API,允许在不刷新页面的情况下与服务器进行通信。尽管现在有了更现代的替代方案,但在某些场景下仍被使用。
基本用法:
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(JSON.parse(xhr.responseText));
}
};
xhr.send();
特点:
- 支持设置请求头、监听上传/下载进度、设置超时等功能。
- 使用回调函数处理异步操作,可能导致“回调地狱”。
- 在现代开发中逐渐被
fetch
替代,但在需要精细控制请求过程时仍有用武之地。
2. Fetch API
fetch
是现代浏览器提供的原生 API,用于替代 XMLHttpRequest
,基于 Promise,更加简洁和强大。
基本用法:
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Fetch error:', error));
特点:
- 基于 Promise,支持 async/await,代码更清晰。
- 默认不发送 cookies,需手动设置
credentials
。 - 不支持直接监听上传/下载进度,需要使用其他方式实现。
- 在现代浏览器中广泛支持,已成为主流的网络请求方式。
常用第三方网络请求库
1. Axios
Axios 是一个基于 Promise 的 HTTP 客户端,可用于浏览器和 Node.js,封装了 XMLHttpRequest
,提供了更丰富的功能。
基本用法:
axios.get('https://api.example.com/data')
.then(response => console.log(response.data))
.catch(error => console.error('Axios error:', error));
特点:
- 自动转换 JSON 数据。
- 支持请求和响应拦截器。
- 支持请求取消、超时设置、防止跨站请求伪造(XSRF)攻击等。
- 在浏览器和 Node.js 中均可使用。
2. Superagent
Superagent 是一个轻量级的 AJAX API,支持 Promise 和回调,适用于浏览器和 Node.js。
基本用法:
superagent.get('https://api.example.com/data')
.then(response => console.log(response.body))
.catch(error => console.error('Superagent error:', error));
特点:
- API 简洁,易于使用。
- 支持链式调用,增强代码可读性。
- 适用于需要快速实现 HTTP 请求的场景。
3. jQuery.ajax
在 jQuery 时代,$.ajax
是实现 AJAX 请求的主要方式,封装了 XMLHttpRequest
,提供了简洁的 API。
基本用法:
$.ajax({
url: 'https://api.example.com/data',
method: 'GET',
success: function(data) {
console.log(data);
},
error: function(error) {
console.error('jQuery AJAX error:', error);
}
});
特点:
- 封装良好,使用方便。
- 依赖于 jQuery,适用于使用 jQuery 的项目。
- 在现代开发中逐渐被原生
fetch
和其他库替代。([CSDN博客][1])
总结
特性 | XMLHttpRequest | Fetch API | Axios | Superagent | jQuery.ajax |
---|---|---|---|---|---|
基于 Promise | 否 | 是 | 是 | 是 | 否 |
请求/响应拦截器 | 否 | 否 | 是 | 否 | 否 |
自动转换 JSON | 否 | 否 | 是 | 否 | 否 |
上传/下载进度监听 | 是 | 否 | 是 | 是 | 是 |
支持取消请求 | 否 | 否 | 是 | 是 | 否 |
浏览器支持 | 广泛 | 现代浏览器 | 广泛 | 广泛 | 广泛 |
Node.js 支持 | 否 | 需 polyfill | 是 | 是 | 否 |
模块化
JavaScript
的模块化是指将复杂的系统分解为多个独立的模块,以提高代码的可维护性、复用性和组织性。随着前端应用的复杂度增加,模块化成为现代 JavaScript
开发的核心。
在早期,JavaScript
主要通过全局变量和函数来组织代码,容易导致命名冲突、依赖管理混乱等问题。模块化通过封装和接口暴露的方式,解决了这些问题。
主要的模块化规范与实现方式
1. CommonJS(主要用于 Node.js)
- 特点:同步加载模块,适用于服务器端。
- 导出模块:使用
module.exports
或exports
。 - 导入模块:使用
require()
。
示例:
// math.js
exports.add = function(a, b) {
return a + b;
};
// main.js
const math = require('./math');
console.log(math.add(2, 3));
CommonJS 规范主要在 Node.js 中使用,适合服务器端的模块化需求。
2. AMD(Asynchronous Module Definition,主要用于浏览器端)
- 特点:异步加载模块,适用于浏览器端。
- 定义模块:使用
define()
。 - 导入模块:使用
require()
。
示例:
// math.js
define([], function() {
return {
add: function(a, b) {
return a + b;
}
};
});
// main.js
require(['math'], function(math) {
console.log(math.add(2, 3));
});
AMD 规范主要由 RequireJS 实现,适合浏览器环境下的模块化需求。
3. CMD(Common Module Definition)
- 特点:依赖就近,延迟执行,适用于浏览器端。
- 定义模块:使用
define()
。 - 导入模块:使用
require()
。
示例:
// math.js
define(function(require, exports, module) {
exports.add = function(a, b) {
return a + b;
};
});
// main.js
define(function(require) {
var math = require('./math');
console.log(math.add(2, 3));
});
CMD 规范主要由 SeaJS 实现,强调依赖的延迟执行。
4. ES6 Modules(ESM,现代浏览器和 Node.js 支持)
- 特点:静态加载,支持编译时优化。
- 导出模块:使用
export
。 - 导入模块:使用
import
。
示例:
// math.js
export function add(a, b) {
return a + b;
}
// main.js
import { add } from './math.js';
console.log(add(2, 3));
ES6 Modules 是 JavaScript 官方标准,现代浏览器和 Node.js 均已支持。
5. UMD(Universal Module Definition)
- 特点:兼容 CommonJS、AMD 和全局变量,适用于库的发布。
- 实现方式:通过判断环境来选择模块化方案。
示例:
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define([], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS
module.exports = factory();
} else {
// 全局变量
root.myModule = factory();
}
}(this, function() {
return {
add: function(a, b) {
return a + b;
}
};
}));
UMD 适用于需要兼容多种模块化环境的库。
模块化规范对比
特性 | CommonJS | AMD | CMD | ES6 Modules | UMD |
---|---|---|---|---|---|
加载方式 | 同步 | 异步 | 异步 | 静态加载 | 兼容多种 |
使用环境 | Node.js | 浏览器 | 浏览器 | 浏览器/Node | 通用 |
导入语法 | require | require | require | import | 自动判断 |
导出语法 | exports | return | exports | export | 自动判断 |
是否支持动态加载 | 否 | 是 | 是 | 是 | 是 |
模块化工具与打包器
在实际开发中,常使用以下工具来实现模块化:
- Webpack:支持多种模块化规范,提供丰富的插件和加载器,适合大型项目。
- Rollup:专注于打包 ES6 模块,生成体积更小的代码,适合库的开发。
- Parcel:零配置的打包工具,适合快速开发和小型项目。
这些工具可以将模块化的代码打包成浏览器可识别的格式,解决兼容性和性能问题。