本篇文章系统整理了 JavaScript 的核心基础知识,包括变量与数据类型、作用域与闭包、原型与原型链、this 指向、执行上下文与执行栈、事件循环、ES6+ 新特性、函数相关、数组方法、对象与深浅拷贝、错误处理、DOM 操作、BOM 操作以及性能优化。每个知识点都配有详细解释和代码示例,适合初学者系统学习,也适合开发者复习查阅。
文章目录
- 一、变量与数据类型
- 二、作用域与闭包
- 三、原型与原型链
- [四、this 指向](#四、this 指向)
-
- [4.1 this 的四种绑定规则](#4.1 this 的四种绑定规则)
-
- [规则 1:默认绑定](#规则 1:默认绑定)
- [规则 2:隐式绑定](#规则 2:隐式绑定)
- [规则 3:显式绑定](#规则 3:显式绑定)
- [规则 4:new 绑定](#规则 4:new 绑定)
- [4.2 优先级](#4.2 优先级)
- [4.3 箭头函数的 this](#4.3 箭头函数的 this)
- 五、执行上下文与执行栈
-
- [5.1 执行上下文类型](#5.1 执行上下文类型)
-
- [1. 全局执行上下文](#1. 全局执行上下文)
- [2. 函数执行上下文](#2. 函数执行上下文)
- [3. eval 执行上下文](#3. eval 执行上下文)
- [5.2 执行上下文的组成](#5.2 执行上下文的组成)
- [5.3 变量提升](#5.3 变量提升)
- [5.4 执行栈](#5.4 执行栈)
- [六、事件循环(Event Loop)](#六、事件循环(Event Loop))
- [七、ES5 核心特性](#七、ES5 核心特性)
-
- [7.1 严格模式(Strict Mode)](#7.1 严格模式(Strict Mode))
- [7.2 JSON 方法](#7.2 JSON 方法)
- [7.3 数组新增方法](#7.3 数组新增方法)
- [7.4 Object 新增方法](#7.4 Object 新增方法)
- [7.5 Function.prototype.bind()](#7.5 Function.prototype.bind())
- [7.6 String.prototype.trim()](#7.6 String.prototype.trim())
- [7.7 Date.prototype.toISOString()](#7.7 Date.prototype.toISOString())
- [7.8 Function.prototype.bind() 的详细说明](#7.8 Function.prototype.bind() 的详细说明)
- [7.9 ES5 的最佳实践](#7.9 ES5 的最佳实践)
-
- [1. 总是使用严格模式](#1. 总是使用严格模式)
- [2. 使用 Array.isArray() 检查数组](#2. 使用 Array.isArray() 检查数组)
- [3. 使用 forEach/map/filter 等方法代替 for 循环](#3. 使用 forEach/map/filter 等方法代替 for 循环)
- [4. 使用 Object.keys() 遍历对象属性](#4. 使用 Object.keys() 遍历对象属性)
- [5. 使用 Object.create() 实现原型继承](#5. 使用 Object.create() 实现原型继承)
- [八、ES6+ 新特性](#八、ES6+ 新特性)
- 九、函数相关
- 十、数组方法
-
- [10.1 改变原数组的方法](#10.1 改变原数组的方法)
- [10.2 不改变原数组的方法](#10.2 不改变原数组的方法)
- [10.3 reduce() 详解](#10.3 reduce() 详解)
- 十一、对象与深浅拷贝
-
- [11.1 浅拷贝](#11.1 浅拷贝)
-
- [方法 1:Object.assign()](#方法 1:Object.assign())
- [方法 2:扩展运算符](#方法 2:扩展运算符)
- [方法 3:Array.slice()(数组)](#方法 3:Array.slice()(数组))
- [11.2 深拷贝](#11.2 深拷贝)
-
- [方法 1:JSON 序列化(简单场景)](#方法 1:JSON 序列化(简单场景))
- [方法 2:递归深拷贝(推荐)](#方法 2:递归深拷贝(推荐))
- [方法 3:使用第三方库](#方法 3:使用第三方库)
- 十二、错误处理
-
- [12.1 try-catch-finally](#12.1 try-catch-finally)
- [12.2 throw 抛出错误](#12.2 throw 抛出错误)
- [12.3 常见错误类型](#12.3 常见错误类型)
- [十三、DOM 操作](#十三、DOM 操作)
-
- [13.1 获取元素](#13.1 获取元素)
- [13.2 操作元素](#13.2 操作元素)
- [13.3 创建和插入元素](#13.3 创建和插入元素)
- [13.4 事件监听](#13.4 事件监听)
- [十四、BOM 操作](#十四、BOM 操作)
-
- [14.1 window 对象](#14.1 window 对象)
- [14.2 location 对象](#14.2 location 对象)
- [14.3 history 对象](#14.3 history 对象)
- [14.4 本地存储](#14.4 本地存储)
- 十五、性能优化
-
- [15.1 代码层面](#15.1 代码层面)
-
- [1. 减少重排重绘](#1. 减少重排重绘)
- [2. 使用事件委托](#2. 使用事件委托)
- [3. 避免内存泄漏](#3. 避免内存泄漏)
- [4. 使用防抖节流](#4. 使用防抖节流)
- [15.2 资源加载](#15.2 资源加载)
-
- [1. 懒加载](#1. 懒加载)
- [2. 预加载](#2. 预加载)
- [15.3 渲染优化](#15.3 渲染优化)
-
- [1. 使用 requestAnimationFrame](#1. 使用 requestAnimationFrame)
- [2. 虚拟 DOM(React)](#2. 虚拟 DOM(React))
- [3. SSR 服务端渲染](#3. SSR 服务端渲染)
一、变量与数据类型
1.1 变量声明方式
JavaScript 提供了三种声明变量的方式,它们各有特点:
var(传统方式)
javascript
var name = '张三';
特点:
- ✅ 函数作用域:只在函数内部有效
- ✅ 变量提升:可以在声明前使用(值为 undefined)
- ✅ 可重复声明:同一作用域内可以多次声明
- ⚠️ 缺点:容易造成变量污染,现代开发中不推荐使用
let(推荐使用)
javascript
let age = 25;
特点:
- ✅ 块级作用域 :只在
{}块内部有效 - ✅ 不存在变量提升:必须先声明后使用
- ✅ 不可重复声明:同一作用域内不能重复声明
- ✅ 暂时性死区(TDZ):从块开始到变量声明期间,无法访问该变量
const(声明常量)
javascript
const PI = 3.1415926;
特点:
- ✅ 块级作用域 :只在
{}块内部有效 - ✅ 声明时必须初始化 :
const a;会报错 - ✅ 不可重新赋值:一旦声明,不能改变指向
- ⚠️ 注意:如果是引用类型(对象、数组),属性可以修改
javascript
// const 引用类型的特殊情况
const person = { name: '张三' };
person.name = '李四'; // ✅ 可以修改属性
person = { name: '王五' }; // ❌ 不能重新赋值
1.2 数据类型详解
JavaScript 的数据类型分为两大类:原始类型和引用类型。
原始类型(7种)
原始类型是不可变 的,存储在栈内存中。
| 类型 | 说明 | 示例 |
|---|---|---|
| String | 字符串 | 'Hello', "World" |
| Number | 数字(整数、浮点数、NaN、Infinity) | 100, 3.14, NaN, Infinity |
| Boolean | 布尔值 | true, false |
| undefined | 未定义 | let a;(a 的值为 undefined) |
| null | 空值 | let b = null; |
| Symbol | 唯一标识符(ES6 新增) | Symbol('id') |
| BigInt | 大整数(ES2020 新增) | 9007199254740991n |
关于 undefined 和 null 的区别:
undefined表示"变量已声明但未赋值"null表示"变量已赋值为空",需要显式赋值
javascript
let a; // undefined
let b = null; // null
console.log(a); // undefined
console.log(b); // null
引用类型
引用类型存储在堆内存中,变量中存储的是内存地址(指针)。
| 类型 | 说明 |
|---|---|
| Object | 对象,包含 Array、Function、Date、RegExp 等 |
javascript
const arr = [1, 2, 3]; // Array 是 Object 的子类型
const date = new Date(); // Date 对象
const reg = /abc/g; // RegExp 对象
const fn = function() {}; // Function 对象
1.3 类型转换
隐式转换
JavaScript 会在运算过程中自动转换类型。
javascript
// + 运算符:数字转字符串
console.log(1 + '2'); // '12'(字符串拼接)
console.log(1 + 2); // 3(数字相加)
// - * / 运算符:字符串转数字
console.log('10' - '2'); // 8
console.log('10' * '2'); // 20
// == 会进行类型转换,=== 严格比较不转换
console.log(1 == '1'); // true(自动转换)
console.log(1 === '1'); // false(严格比较)
console.log(null == undefined); // true(特殊情况)
console.log(null === undefined); // false
显式转换
使用内置函数进行类型转换。
javascript
// Number():转为数字
console.log(Number('123')); // 123
console.log(Number('abc')); // NaN
console.log(Number('')); // 0
// String():转为字符串
console.log(String(123)); // '123'
console.log(String(true)); // 'true'
// Boolean():转为布尔值
console.log(Boolean(0)); // false
console.log(Boolean('')); // false
console.log(Boolean(null)); // false
console.log(Boolean(undefined));// false
console.log(Boolean('abc')); // true
// parseInt():从字符串中解析整数
console.log(parseInt('123abc')); // 123
console.log(parseInt('abc123')); // NaN
console.log(parseInt('10.5')); // 10
// parseFloat():从字符串中解析浮点数
console.log(parseFloat('10.5')); // 10.5
** falsy 值**:以下值在布尔转换时会转为 false:
0、-0、0n''(空字符串)nullundefinedNaN
二、作用域与闭包
2.1 作用域类型
作用域决定了变量和函数的可访问范围。
全局作用域
在函数外部声明的变量,整个程序都可以访问。
javascript
const globalVar = '全局变量';
function fn() {
console.log(globalVar); // ✅ 可以访问
}
fn();
console.log(globalVar); // ✅ 可以访问
函数作用域
在函数内部声明的变量,只能在函数内部访问。
javascript
function fn() {
const localVar = '局部变量';
console.log(localVar); // ✅ 可以访问
}
fn();
console.log(localVar); // ❌ 报错:localVar is not defined
块级作用域
let 和 const 声明的变量只在 {} 块内部有效。
javascript
{
let blockVar = '块级变量';
const blockConst = '块级常量';
console.log(blockVar); // ✅ 可以访问
console.log(blockConst); // ✅ 可以访问
}
console.log(blockVar); // ❌ 报错:blockVar is not defined
console.log(blockConst); // ❌ 报错:blockConst is not defined
javascript
// 经典案例:for 循环中的 var 和 let
// 使用 var
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 输出:3, 3, 3
}
// 使用 let(推荐)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 输出:0, 1, 2
}
2.2 作用域链
当访问一个变量时,JavaScript 会沿着作用域链逐层向上查找,直到找到该变量或到达全局作用域。
javascript
let globalVar = '全局变量';
function outer() {
let outerVar = '外部变量';
function inner() {
let innerVar = '内部变量';
console.log(innerVar); // ✅ 在当前作用域找到
console.log(outerVar); // ✅ 向上查找,在 outer 作用域找到
console.log(globalVar); // ✅ 继续向上,在全局作用域找到
console.log(notExist); // ❌ 找不到,报错
}
inner();
}
outer();
2.3 闭包
什么是闭包?
闭包是指函数能够记住并访问其词法作用域,即使函数在其词法作用域之外执行。
简单来说:内部函数引用了外部函数的变量,外部函数执行完毕后,变量不会被销毁,依然存在于内存中。
javascript
function outer() {
let count = 0;
function inner() {
count++;
console.log(count);
}
return inner;
}
const counter = outer();
counter(); // 输出:1
counter(); // 输出:2
counter(); // 输出:3
闭包的原理:
outer函数执行完毕后,count变量本应被销毁- 但
inner函数被返回并赋值给counter inner函数引用了count变量- 因此
count变量不会被垃圾回收,一直存在于内存中
闭包的经典应用场景
场景 1:数据私有化(封装)
javascript
function createCounter() {
let count = 0; // 私有变量,外部无法直接访问
return {
increment() {
count++;
console.log('增加后:', count);
return count;
},
decrement() {
count--;
console.log('减少后:', count);
return count;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
counter.increment(); // 输出:1
counter.increment(); // 输出:2
counter.decrement(); // 输出:1
console.log('当前值:', counter.getCount()); // 输出:1
// counter.count; // ❌ 无法直接访问 count 变量
场景 2:模块模式
javascript
const module = (function() {
let privateVar = '我是私有变量';
function privateMethod() {
console.log('私有方法');
}
return {
publicMethod() {
console.log('公共方法');
console.log(privateVar); // 可以访问私有变量
privateMethod(); // 可以调用私有方法
}
};
})();
module.publicMethod(); // ✅ 可以调用
// module.privateMethod(); // ❌ 无法调用
场景 3:函数柯里化(参数复用)
javascript
function multiply(a) {
return function(b) {
return a * b;
};
}
const double = multiply(2); // 创建一个函数,用于计算两倍
const triple = multiply(3); // 创建一个函数,用于计算三倍
console.log(double(5)); // 输出:10
console.log(triple(5)); // 输出:15
场景 4:节流防抖
javascript
// 防抖:多次触发只执行最后一次
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 节流:固定时间间隔执行
function throttle(fn, delay) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
lastTime = now;
fn.apply(this, args);
}
};
}
闭包的注意事项
优点:
- ✅ 实现数据私有化,避免全局变量污染
- ✅ 实现模块化开发
- ✅ 实现函数柯里化、节流防抖等功能
缺点:
- ⚠️ 内存泄漏风险:闭包会导致外部函数的变量一直存在于内存中,不会被销毁
- ⚠️ 性能影响:大量闭包可能导致内存占用过高
解决方法:
- 及时将闭包引用置为
null - 避免不必要的闭包
javascript
const counter = createCounter();
counter.increment(); // 使用完毕
counter = null; // 手动释放,帮助垃圾回收
三、原型与原型链
3.1 原型基础
JavaScript 的继承是基于原型链的,理解原型是掌握 JavaScript 的关键。
核心概念
1. prototype(显式原型)
- 每个函数都有一个
prototype属性 prototype是一个对象,用于实现属性和方法的继承
javascript
function Person() {}
console.log(Person.prototype); // Person {}
Person.prototype.name = '默认名字';
Person.prototype.sayHello = function() {
console.log('Hello');
};
console.log(Person.prototype.name); // '默认名字'
2. proto(隐式原型)
- 每个对象都有一个
__proto__属性 __proto__指向构造函数的prototype
javascript
function Person() {}
const p = new Person();
console.log(p.__proto__ === Person.prototype); // true
3. constructor(构造器)
prototype对象有一个constructor属性constructor指向构造函数本身
javascript
function Person() {}
console.log(Person.prototype.constructor === Person); // true
const p = new Person();
console.log(p.constructor === Person); // true
3.2 原型链
原型链是由 __proto__ 连接而成的链式结构,用于查找属性和方法。
原型链示意图
p (实例对象)
└── __proto__ ──> Person.prototype (原型对象)
├── constructor: Person
├── name: '默认名字'
├── sayHello()
└── __proto__ ──> Object.prototype
├── hasOwnProperty()
├── toString()
├── valueOf()
└── __proto__ ──> null
核心关系
javascript
function Person() {}
const p = new Person();
// 关系 1:实例对象的 __proto__ 指向构造函数的 prototype
console.log(p.__proto__ === Person.prototype); // true
// 关系 2:构造函数的 prototype 的 __proto__ 指向 Object.prototype
console.log(Person.prototype.__proto__ === Object.prototype); // true
// 关系 3:Object.prototype 的 __proto__ 指向 null(原型链顶端)
console.log(Object.prototype.__proto__ === null); // true
// 关系 4:构造函数的 prototype 的 constructor 指向构造函数本身
console.log(Person.prototype.constructor === Person); // true
属性查找过程
当访问对象的属性或方法时,会沿着原型链逐层向上查找:
javascript
function Person() {}
Person.prototype.name = '原型上的名字';
Person.prototype.sayHello = function() {
console.log('Hello');
};
const p = new Person();
p.name = '实例上的名字';
console.log(p.name); // '实例上的名字'(先在实例上找到)
delete p.name;
console.log(p.name); // '原型上的名字'(实例上没有,去原型上找)
查找顺序:
- 先在对象本身查找
- 如果没有,去
__proto__指向的原型对象上查找 - 如果还没有,继续向上查找,直到
Object.prototype - 如果
Object.prototype上也没有,返回undefined
3.3 继承实现方式
JavaScript 有多种继承方式,各有优缺点。
方式 1:原型链继承
javascript
function Parent() {
this.name = 'parent';
this.hobbies = ['reading', 'coding'];
}
function Child() {}
Child.prototype = new Parent(); // 关键:将 Child 的原型指向 Parent 的实例
const child1 = new Child();
const child2 = new Child();
child1.hobbies.push('gaming');
console.log(child1.hobbies); // ['reading', 'coding', 'gaming']
console.log(child2.hobbies); // ['reading', 'coding', 'gaming'] ⚠️ 引用类型被共享
缺点:
- ❌ 引用类型的属性会被所有实例共享
- ❌ 创建子类实例时,无法向父类构造函数传参
方式 2:构造函数继承
javascript
function Parent(name) {
this.name = name;
this.hobbies = ['reading', 'coding'];
}
function Child(name) {
Parent.call(this, name); // 关键:调用父类构造函数,绑定 this
}
const child1 = new Child('child1');
const child2 = new Child('child2');
child1.hobbies.push('gaming');
console.log(child1.name); // 'child1'
console.log(child2.name); // 'child2'
console.log(child1.hobbies); // ['reading', 'coding', 'gaming']
console.log(child2.hobbies); // ['reading', 'coding'] ✅ 引用类型独立
缺点:
- ❌ 无法继承父类原型上的方法
javascript
Parent.prototype.sayHello = function() {
console.log('Hello');
};
const child = new Child('child');
child.sayHello(); // ❌ 报错:child.sayHello is not a function
方式 3:组合继承(推荐)
结合了原型链继承和构造函数继承的优点。
javascript
function Parent(name) {
this.name = name;
this.hobbies = ['reading', 'coding'];
}
Parent.prototype.sayHello = function() {
console.log('Hello, I am', this.name);
};
function Child(name, age) {
Parent.call(this, name); // 继承实例属性
this.age = age;
}
Child.prototype = new Parent(); // 继承原型方法
Child.prototype.constructor = Child; // 修正 constructor 指向
const child = new Child('张三', 18);
child.hobbies.push('gaming');
child.sayHello(); // 'Hello, I am 张三' ✅ 可以调用父类方法
console.log(child.hobbies); // ['reading', 'coding', 'gaming']
优点:
- ✅ 可以继承实例属性
- ✅ 可以继承原型方法
- ✅ 向父类构造函数传参
缺点:
- ⚠️ 父类构造函数被调用两次(创建子类原型时、创建子类实例时)
方式 4:ES6 Class 继承(最推荐)
javascript
class Parent {
constructor(name) {
this.name = name;
this.hobbies = ['reading', 'coding'];
}
sayHello() {
console.log('Hello, I am', this.name);
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 必须调用 super()
this.age = age;
}
sayAge() {
console.log('I am', this.age, 'years old');
}
}
const child = new Child('张三', 18);
child.hobbies.push('gaming');
child.sayHello(); // 'Hello, I am 张三'
child.sayAge(); // 'I am 18 years old'
console.log(child.hobbies); // ['reading', 'coding', 'gaming']
优点:
- ✅ 语法简洁,易于理解
- ✅ 不会调用两次父类构造函数
- ✅ 支持静态方法继承
四、this 指向
this 是 JavaScript 中最让人困惑的概念之一,但掌握它的规律后,其实非常清晰。
4.1 this 的四种绑定规则
规则 1:默认绑定
独立函数调用时,this 指向全局对象(非严格模式下)或 undefined(严格模式下)。
javascript
function fn() {
console.log(this);
}
fn(); // 非严格模式:Window 对象
// 严格模式:undefined
规则 2:隐式绑定
作为对象的方法调用时,this 指向该对象。
javascript
const obj = {
name: '张三',
fn() {
console.log(this.name);
}
};
obj.fn(); // '张三'(this 指向 obj)
隐式丢失 :当方法被赋值给变量或作为参数传递时,this 会丢失。
javascript
const obj = {
name: '张三',
fn() {
console.log(this.name);
}
};
const fn = obj.fn;
fn(); // undefined(this 不再指向 obj)
// 经典案例
const button = document.querySelector('button');
button.addEventListener('click', obj.fn);
// 点击时输出:undefined(this 不指向 obj)
规则 3:显式绑定
使用 call()、apply()、bind() 方法改变 this 指向。
javascript
const obj = { name: '张三' };
function fn(greeting) {
console.log(greeting + ',', this.name);
}
// call():逐个传递参数
fn.call(obj, '你好'); // '你好, 张三'
// apply():通过数组传递参数
fn.apply(obj, ['你好']); // '你好, 张三'
// bind():返回新函数,不立即执行
const boundFn = fn.bind(obj, '你好');
boundFn(); // '你好, 张三'
区别:
call()和apply()立即执行bind()返回新函数,稍后执行call()逐个传参,apply()数组传参
规则 4:new 绑定
使用 new 调用构造函数时,this 指向新创建的对象。
javascript
function Person(name) {
this.name = name;
}
const p = new Person('张三');
console.log(p.name); // '张三'(this 指向新创建的对象 p)
4.2 优先级
new > 显式绑定 > 隐式绑定 > 默认绑定
javascript
// 优先级示例
function fn() {
console.log(this.name);
}
const obj1 = { name: 'obj1' };
const obj2 = { name: 'obj2' };
// 隐式绑定
obj1.fn = fn;
obj1.fn(); // 'obj1'
// 显式绑定 > 隐式绑定
obj1.fn.call(obj2); // 'obj2'
// new 绑定 > 显式绑定
const boundFn = fn.bind(obj2);
new boundFn(); // undefined(this 指向新创建的对象,不是 obj2)
4.3 箭头函数的 this
箭头函数没有自己的 this ,它的 this 继承自外层作用域,且无法通过 call、apply、bind 改变。
javascript
const obj = {
name: '张三',
fn() {
// 普通函数的 this
setTimeout(function() {
console.log(this.name); // undefined(this 指向全局对象)
}, 100);
// 箭头函数的 this
setTimeout(() => {
console.log(this.name); // '张三'(this 继承自外层 fn 的 this)
}, 100);
}
};
obj.fn();
箭头函数适用场景:
- ✅ 回调函数中需要保持
this指向 - ✅ 事件监听器中需要保持
this指向
javascript
class Button {
constructor() {
this.count = 0;
this.button = document.querySelector('button');
// 普通函数:this 会丢失
// this.button.addEventListener('click', this.handleClick);
// 箭头函数:this 保持指向 Button 实例
this.button.addEventListener('click', () => this.handleClick());
}
handleClick() {
this.count++;
console.log('点击次数:', this.count);
}
}
五、执行上下文与执行栈
5.1 执行上下文类型
执行上下文是 JavaScript 代码执行时的环境,分为三种类型:
1. 全局执行上下文
- 程序启动时创建
- 程序结束时销毁
- 只有一个
javascript
// 整个文件运行时处于全局执行上下文
const globalVar = '全局变量';
function fn() {
// 函数执行时创建函数执行上下文
const localVar = '局部变量';
}
2. 函数执行上下文
- 函数调用时创建
- 函数执行完毕后销毁
- 可以有多个
3. eval 执行上下文
- 不推荐使用
eval()函数执行时创建
5.2 执行上下文的组成
每个执行上下文包含三个部分:
javascript
ExecutionContext = {
ThisBinding: <this value>, // this 指向
VariableEnvironment: { ... }, // 变量环境(var、函数声明)
LexicalEnvironment: { ... } // 词法环境(let、const、块级作用域)
}
变量环境 vs 词法环境:
- 变量环境 :存储
var声明的变量和函数声明 - 词法环境 :存储
let、const声明的变量和块级作用域
javascript
function fn() {
var a = 1; // 存储在变量环境
let b = 2; // 存储在词法环境
const c = 3; // 存储在词法环境
{
let d = 4; // 存储在块级词法环境
console.log(d); // 4
}
// console.log(d); // 报错:d is not defined
}
5.3 变量提升
JavaScript 在代码执行前,会先将变量和函数声明提升到当前作用域顶部。
var 的变量提升:
javascript
console.log(a); // undefined(变量声明提升,但未赋值)
var a = 1;
console.log(a); // 1
// 等价于
var a;
console.log(a); // undefined
a = 1;
console.log(a); // 1
let 和 const 不存在变量提升:
javascript
console.log(b); // ❌ 报错:Cannot access 'b' before initialization(暂时性死区)
let b = 2;
函数声明提升:
javascript
console.log(fn1); // [Function: fn1]
fn1(); // 'fn1'
// 函数表达式不会提升
console.log(fn2); // undefined
// fn2(); // ❌ 报错:fn2 is not a function
function fn1() {
console.log('fn1');
}
const fn2 = function() {
console.log('fn2');
};
5.4 执行栈
执行栈(调用栈)是用于管理函数调用的 LIFO(后进先出)结构。
javascript
function fn1() {
console.log('fn1 开始');
fn2();
console.log('fn1 结束');
}
function fn2() {
console.log('fn2 开始');
fn3();
console.log('fn2 结束');
}
function fn3() {
console.log('fn3');
}
fn1();
// 执行过程:
// 1. fn1 入栈,输出 'fn1 开始'
// 2. fn2 入栈,输出 'fn2 开始'
// 3. fn3 入栈,输出 'fn3'
// 4. fn3 出栈
// 5. 输出 'fn2 结束',fn2 出栈
// 6. 输出 'fn1 结束',fn1 出栈
// 最终输出:
// fn1 开始
// fn2 开始
// fn3
// fn2 结束
// fn1 结束
栈溢出:递归调用过深时,栈空间不足。
javascript
function recursive() {
recursive();
}
recursive(); // RangeError: Maximum call stack size exceeded
六、事件循环(Event Loop)
事件循环是 JavaScript 异步编程的核心机制。
6.1 任务队列类型
JavaScript 的任务分为两类:
宏任务
- script(整体代码)
- setTimeout、setInterval
- I/O 操作
- UI 渲染
微任务
- Promise.then、Promise.catch、Promise.finally
- MutationObserver(监听 DOM 变化)
- process.nextTick(Node.js 环境)
6.2 执行顺序
javascript
console.log('1. 同步代码');
setTimeout(() => console.log('2. setTimeout'), 0);
Promise.resolve().then(() => console.log('3. Promise.then'));
console.log('4. 同步代码');
// 输出顺序:
// 1. 同步代码
// 4. 同步代码
// 3. Promise.then
// 2. setTimeout
执行规则:
- 执行同步代码
- 同步代码执行完毕,栈清空
- 执行所有微任务(微任务队列清空)
- 执行一个宏任务
- 重复步骤 3-4
6.3 经典面试题
javascript
setTimeout(() => console.log('1. setTimeout 1'), 0);
Promise.resolve().then(() => {
console.log('2. Promise 1');
Promise.resolve().then(() => console.log('3. Promise 2'));
});
setTimeout(() => console.log('4. setTimeout 2'), 0);
console.log('5. 同步代码');
// 输出顺序:
// 5. 同步代码
// 2. Promise 1
// 3. Promise 2(微任务执行过程中产生的微任务,会在当前轮次执行)
// 1. setTimeout 1
// 4. setTimeout 2
javascript
setTimeout(() => console.log('1. setTimeout'), 0);
new Promise(resolve => {
console.log('2. Promise 构造函数');
resolve();
}).then(() => {
console.log('3. Promise.then 1');
Promise.resolve().then(() => console.log('4. Promise.then 2'));
});
console.log('5. 同步代码');
// 输出顺序:
// 2. Promise 构造函数(同步执行)
// 5. 同步代码
// 3. Promise.then 1
// 4. Promise.then 2
// 1. setTimeout
七、ES5 核心特性
ES5(ECMAScript 5)发布于 2009 年,是 JavaScript 的一个重要版本,引入了许多核心特性,为现代 JavaScript 开发奠定了基础。
7.1 严格模式(Strict Mode)
严格模式是 ES5 引入的一种运行模式,它使 JavaScript 在更严格的条件下运行。
开启严格模式
在脚本或函数开头添加 'use strict'; 即可开启严格模式。
javascript
// 全局严格模式
'use strict';
function fn() {
// 函数严格模式
'use strict';
// ...
}
严格模式的主要限制
| 限制 | 说明 | 示例 |
|---|---|---|
| 禁止使用未声明的变量 | 必须先声明后使用 | x = 1; ❌ 报错 |
| 禁止删除变量或函数 | 不能删除变量、函数、函数参数 | delete x; ❌ 报错 |
| 禁止重名参数 | 函数参数不能重名 | function fn(a, a) {} ❌ 报错 |
| 禁止八进制字面量 | 0 开头的八进制语法被禁止 |
var x = 012; ❌ 报错 |
| 禁止 with 语句 | with 语句被完全禁止 |
with(obj) {} ❌ 报错 |
| eval 更严格 | eval 不会创建全局变量 |
eval('var x = 1'); x 不会被创建 |
| this 默认为 undefined | 独立函数调用时 this 为 undefined | function fn() { console.log(this); } fn(); // undefined |
| 保留字 | 不能使用保留字作为变量名 | var private = 1; ❌ 可能报错 |
javascript
'use strict';
// ❌ 未声明的变量
x = 1; // ReferenceError: x is not defined
// ❌ 删除变量
var y = 1;
delete y; // SyntaxError: Delete of an unqualified identifier
// ❌ 重名参数
function fn(a, a) {} // SyntaxError: Duplicate parameter name
// ❌ 八进制字面量
var octal = 012; // SyntaxError: Octal literals are not allowed
// ❌ with 语句
var obj = { a: 1 };
with (obj) { a = 2; } // SyntaxError: Strict mode code may not include a with statement
// ✅ this 默认为 undefined
function fn() {
console.log(this); // undefined
}
fn();
// ✅ eval 更严格
eval('var z = 1;');
console.log(z); // ReferenceError: z is not defined
7.2 JSON 方法
ES5 引入了 JSON 对象,用于 JSON 数据的解析和序列化。
JSON.stringify()
将 JavaScript 对象或值转换为 JSON 字符串。
javascript
const obj = {
name: '张三',
age: 18,
hobbies: ['reading', 'coding']
};
// 基本用法
const jsonStr = JSON.stringify(obj);
console.log(jsonStr);
// '{"name":"张三","age":18,"hobbies":["reading","coding"]}'
// 格式化输出(第二个参数:缩进空格数)
const formatted = JSON.stringify(obj, null, 2);
console.log(formatted);
// {
// "name": "张三",
// "age": 18,
// "hobbies": [
// "reading",
// "coding"
// ]
// }
// 过滤属性(第二个参数:数组或函数)
const filtered = JSON.stringify(obj, ['name', 'age']);
console.log(filtered); // '{"name":"张三","age":18}'
// 使用函数过滤
const filtered2 = JSON.stringify(obj, (key, value) => {
if (key === 'hobbies') {
return undefined; // 过滤掉 hobbies
}
return value;
});
console.log(filtered2); // '{"name":"张三","age":18}'
JSON.parse()
将 JSON 字符串解析为 JavaScript 对象。
javascript
const jsonStr = '{"name":"张三","age":18}';
// 基本用法
const obj = JSON.parse(jsonStr);
console.log(obj); // { name: '张三', age: 18 }
// 使用 reviver 函数转换值
const obj2 = JSON.parse(jsonStr, (key, value) => {
if (key === 'age') {
return value + 1; // 年龄加 1
}
return value;
});
console.log(obj2); // { name: '张三', age: 19 }
7.3 数组新增方法
ES5 为数组添加了许多实用方法。
Array.isArray()
判断一个值是否为数组。
javascript
Array.isArray([1, 2, 3]); // true
Array.isArray('hello'); // false
Array.isArray(new Array()); // true
Array.isArray({}); // false
// 更好的替代方案(ES5 之前)
function isArray(obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
}
Array.prototype.forEach()
遍历数组,对每个元素执行回调函数。
javascript
const arr = [1, 2, 3];
arr.forEach(function(item, index, array) {
console.log(item, index, array);
});
// 输出:
// 1 0 [1, 2, 3]
// 2 1 [1, 2, 3]
// 3 2 [1, 2, 3]
// 注意:forEach 无法 break 或 return
arr.forEach(function(item) {
if (item === 2) {
return; // 只跳过当前元素,不是跳出循环
}
console.log(item);
});
// 输出:1, 3
Array.prototype.map()
创建一个新数组,包含回调函数的返回值。
javascript
const arr = [1, 2, 3];
const doubled = arr.map(function(item, index, array) {
return item * 2;
});
console.log(doubled); // [2, 4, 6]
console.log(arr); // [1, 2, 3](原数组不变)
Array.prototype.filter()
创建一个新数组,包含通过测试的元素。
javascript
const arr = [1, 2, 3, 4, 5];
const evens = arr.filter(function(item, index, array) {
return item % 2 === 0;
});
console.log(evens); // [2, 4]
Array.prototype.reduce()
归约数组,返回一个累加值。
javascript
const arr = [1, 2, 3, 4, 5];
// 求和
const sum = arr.reduce(function(accumulator, currentValue, index, array) {
return accumulator + currentValue;
}, 0);
console.log(sum); // 15
// 求最大值
const max = arr.reduce(function(accumulator, currentValue) {
return Math.max(accumulator, currentValue);
}, -Infinity);
console.log(max); // 5
Array.prototype.reduceRight()
从右向左归约数组。
javascript
const arr = ['a', 'b', 'c'];
const result = arr.reduceRight(function(accumulator, currentValue) {
return accumulator + currentValue;
}, '');
console.log(result); // 'cba'
Array.prototype.every()
测试数组中的所有元素是否都通过测试。
javascript
const arr = [1, 2, 3, 4, 5];
const allPositive = arr.every(function(item) {
return item > 0;
});
console.log(allPositive); // true
const allGreaterThan3 = arr.every(function(item) {
return item > 3;
});
console.log(allGreaterThan3); // false
Array.prototype.some()
测试数组中是否至少有一个元素通过测试。
javascript
const arr = [1, 2, 3, 4, 5];
const hasEven = arr.some(function(item) {
return item % 2 === 0;
});
console.log(hasEven); // true
Array.prototype.indexOf()
返回指定元素的首次出现索引,不存在则返回 -1。
javascript
const arr = [1, 2, 3, 2, 1];
console.log(arr.indexOf(2)); // 1
console.log(arr.indexOf(4)); // -1
console.log(arr.indexOf(2, 2)); // 3(从索引 2 开始查找)
Array.prototype.lastIndexOf()
返回指定元素的最后一次出现索引。
javascript
const arr = [1, 2, 3, 2, 1];
console.log(arr.lastIndexOf(2)); // 3
console.log(arr.lastIndexOf(4)); // -1
7.4 Object 新增方法
Object.create()
使用指定的原型对象和属性创建一个新对象。
javascript
// 创建一个原型为 null 的对象
const obj1 = Object.create(null);
console.log(obj1); // {}(没有原型)
// 创建一个原型为 Person.prototype 的对象
const Person = {
greet: function() {
console.log('Hello, I am', this.name);
}
};
const person = Object.create(Person);
person.name = '张三';
person.greet(); // 'Hello, I am 张三'
// 使用第二个参数定义属性
const obj2 = Object.create(Person.prototype, {
name: {
value: '李四',
writable: true,
enumerable: true,
configurable: true
},
age: {
value: 18,
writable: true
}
});
console.log(obj2.name); // '李四'
console.log(obj2.age); // 18
Object.defineProperty()
直接在对象上定义一个新属性,或修改现有属性。
javascript
const obj = {};
// 定义属性
Object.defineProperty(obj, 'name', {
value: '张三',
writable: true, // 可写
enumerable: true, // 可枚举
configurable: true // 可配置
});
console.log(obj.name); // '张三'
// 使用 getter 和 setter
let ageValue = 18;
Object.defineProperty(obj, 'age', {
get: function() {
console.log('获取 age');
return ageValue;
},
set: function(value) {
console.log('设置 age 为', value);
ageValue = value;
},
enumerable: true,
configurable: true
});
obj.age = 20; // 输出:设置 age 为 20
console.log(obj.age); // 输出:获取 age 20
// 定义只读属性
Object.defineProperty(obj, 'PI', {
value: 3.1415926,
writable: false
});
obj.PI = 3.14; // 严格模式下会报错
console.log(obj.PI); // 3.1415926
Object.defineProperties()
一次定义多个属性。
javascript
const obj = {};
Object.defineProperties(obj, {
name: {
value: '张三',
writable: true
},
age: {
value: 18,
writable: true
},
city: {
value: '北京',
enumerable: false // 不可枚举
}
});
console.log(obj.name); // '张三'
console.log(obj.age); // 18
console.log(obj.city); // '北京'
for (const key in obj) {
console.log(key); // name, age(city 不可枚举)
}
Object.getOwnPropertyNames()
返回对象的所有自身属性(包括不可枚举属性)。
javascript
const obj = { a: 1, b: 2 };
Object.defineProperty(obj, 'c', {
value: 3,
enumerable: false
});
console.log(Object.keys(obj)); // ['a', 'b'](只返回可枚举属性)
console.log(Object.getOwnPropertyNames(obj)); // ['a', 'b', 'c'](包含不可枚举属性)
Object.getPrototypeOf()
返回对象的原型。
javascript
const obj = {};
const prototype = Object.getPrototypeOf(obj);
console.log(prototype === Object.prototype); // true
console.log(prototype === Object.getPrototypeOf({})); // true
Object.setPrototypeOf()
设置对象的原型(不推荐使用,可能影响性能)。
javascript
const obj = {};
const proto = { greet: function() { console.log('Hello'); } };
Object.setPrototypeOf(obj, proto);
obj.greet(); // 'Hello'
Object.keys()
返回对象的所有可枚举属性。
javascript
const obj = { name: '张三', age: 18 };
console.log(Object.keys(obj)); // ['name', 'age']
// 结合数组方法使用
const keys = Object.keys(obj);
keys.forEach(function(key) {
console.log(key, obj[key]);
});
// name 张三
// age 18
Object.freeze()
冻结对象,使其不可修改。
javascript
const obj = { name: '张三', age: 18 };
Object.freeze(obj);
obj.name = '李四'; // 严格模式下报错
delete obj.age; // 严格模式下报错
obj.city = '北京'; // 严格模式下报错
console.log(obj); // { name: '张三', age: 18 }
// 检查是否冻结
console.log(Object.isFrozen(obj)); // true
Object.seal()
密封对象,防止添加或删除属性,但可以修改现有属性。
javascript
const obj = { name: '张三', age: 18 };
Object.seal(obj);
obj.name = '李四'; // ✅ 可以修改
delete obj.age; // ❌ 不能删除
obj.city = '北京'; // ❌ 不能添加
console.log(obj); // { name: '李四', age: 18 }
// 检查是否密封
console.log(Object.isSealed(obj)); // true
Object.preventExtensions()
防止向对象添加新属性。
javascript
const obj = { name: '张三' };
Object.preventExtensions(obj);
obj.name = '李四'; // ✅ 可以修改
delete obj.name; // ✅ 可以删除
obj.age = 18; // ❌ 不能添加
console.log(obj); // { name: '李四' }
// 检查是否可扩展
console.log(Object.isExtensible(obj)); // false
Object.getOwnPropertyDescriptor()
返回指定属性的属性描述符。
javascript
const obj = { name: '张三' };
const descriptor = Object.getOwnPropertyDescriptor(obj, 'name');
console.log(descriptor);
// {
// value: '张三',
// writable: true,
// enumerable: true,
// configurable: true
// }
Object.getOwnPropertyDescriptors()
返回对象所有自身属性的属性描述符。
javascript
const obj = { name: '张三', age: 18 };
const descriptors = Object.getOwnPropertyDescriptors(obj);
console.log(descriptors);
// {
// name: {
// value: '张三',
// writable: true,
// enumerable: true,
// configurable: true
// },
// age: { ... }
// }
7.5 Function.prototype.bind()
创建一个新函数,绑定的 this 指向指定对象。
javascript
const obj = { name: '张三' };
function fn(greeting) {
console.log(greeting + ',', this.name);
}
// 创建绑定函数
const boundFn = fn.bind(obj);
boundFn('你好'); // '你好, 张三'
// 预设参数(部分应用)
const greetHello = fn.bind(obj, '你好');
greetHello(); // '你好, 张三'
// 多次 bind 只有第一次有效
const obj2 = { name: '李四' };
const boundFn2 = fn.bind(obj).bind(obj2);
boundFn2('你好'); // '你好, 张三'(仍然指向 obj)
7.6 String.prototype.trim()
去除字符串两端的空白字符。
javascript
const str = ' Hello, World! ';
console.log(str.trim()); // 'Hello, World!'
console.log(str.trimLeft()); // 'Hello, World! '(非标准)
console.log(str.trimRight()); // ' Hello, World!'(非标准)
7.7 Date.prototype.toISOString()
返回 ISO 格式的日期字符串。
javascript
const date = new Date();
console.log(date.toISOString()); // '2024-01-01T00:00:00.000Z'
7.8 Function.prototype.bind() 的详细说明
bind() 方法创建一个新的函数,当这个新函数被调用时,它的 this 值被永久绑定到 bind() 的第一个参数。
javascript
const module = {
x: 42,
getX: function() {
return this.x;
}
};
const unboundGetX = module.getX;
console.log(unboundGetX()); // undefined(this 指向全局对象)
const boundGetX = unboundGetX.bind(module);
console.log(boundGetX()); // 42(this 指向 module)
7.9 ES5 的最佳实践
1. 总是使用严格模式
javascript
'use strict';
// ... 你的代码
2. 使用 Array.isArray() 检查数组
javascript
if (Array.isArray(arr)) {
// 处理数组
}
3. 使用 forEach/map/filter 等方法代替 for 循环
javascript
// ❌ 传统的 for 循环
for (var i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
// ✅ ES5 的 forEach
arr.forEach(function(item) {
console.log(item);
});
4. 使用 Object.keys() 遍历对象属性
javascript
// ✅ 遍历对象
Object.keys(obj).forEach(function(key) {
console.log(key, obj[key]);
});
5. 使用 Object.create() 实现原型继承
javascript
function Parent(name) {
this.name = name;
}
Parent.prototype.greet = function() {
console.log('Hello, I am', this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
const child = new Child('张三', 18);
child.greet(); // 'Hello, I am 张三'
八、ES6+ 新特性
8.1 解构赋值
数组解构
javascript
const arr = [1, 2, 3];
const [a, b, c] = arr;
console.log(a, b, c); // 1 2 3
// 跳过元素
const [x, , z] = arr;
console.log(x, z); // 1 3
// 设置默认值
const [m, n, o = 10] = [1, 2];
console.log(m, n, o); // 1 2 10
// 剩余元素
const [first, ...rest] = arr;
console.log(first, rest); // 1 [2, 3]
对象解构
javascript
const obj = { name: '张三', age: 18, city: '北京' };
const { name, age } = obj;
console.log(name, age); // 张三 18
// 重命名
const { name: userName, age: userAge } = obj;
console.log(userName, userAge); // 张三 18
// 设置默认值
const { name, gender = '男' } = obj;
console.log(name, gender); // 张三 男
8.2 箭头函数
javascript
// 基本语法
const add = (a, b) => a + b;
// 多行代码需要用 {}
const multiply = (a, b) => {
const result = a * b;
return result;
};
// 单个参数可以省略 ()
const square = x => x * x;
// 无参数需要用 ()
const sayHello = () => console.log('Hello');
// 返回对象需要用 ()
const getUser = name => ({ name, age: 18 });
console.log(getUser('张三')); // { name: '张三', age: 18 }
8.3 模板字符串
javascript
const name = '张三';
const age = 18;
// 使用 ` ` 和 ${}
const message = `我叫${name},今年${age}岁`;
// 支持多行
const html = `
<div>
<h1>${name}</h1>
<p>年龄:${age}</p>
</div>
`;
8.4 扩展运算符
javascript
// 数组
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
// 合并数组
const arr3 = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
// 复制数组(浅拷贝)
const arr4 = [...arr1];
// 转换伪数组为数组
const nodeList = document.querySelectorAll('div');
const arr5 = [...nodeList];
// 对象
const obj1 = { name: '张三', age: 18 };
const obj2 = { city: '北京' };
// 合并对象
const obj3 = { ...obj1, ...obj2 }; // { name: '张三', age: 18, city: '北京' }
// 复制对象(浅拷贝)
const obj4 = { ...obj1 };
8.5 Promise
Promise 是处理异步操作的对象,有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。
javascript
// 创建 Promise
const promise = new Promise((resolve, reject) => {
// 模拟异步操作
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
resolve('操作成功');
} else {
reject('操作失败');
}
}, 1000);
});
// 使用 Promise
promise
.then(result => {
console.log('成功:', result);
return result + ',继续处理';
})
.then(result => {
console.log('链式调用:', result);
})
.catch(error => {
console.error('失败:', error);
})
.finally(() => {
console.log('无论成功失败都会执行');
});
Promise 静态方法:
javascript
// Promise.all:所有 Promise 都成功才成功
Promise.all([
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
]).then(results => {
console.log(results); // [1, 2, 3]
});
// Promise.race:哪个先完成就返回哪个
Promise.race([
new Promise(resolve => setTimeout(() => resolve(1), 100)),
new Promise(resolve => setTimeout(() => resolve(2), 200))
]).then(result => {
console.log(result); // 1
});
// Promise.allSettled:返回所有 Promise 的结果(无论成功失败)
Promise.allSettled([
Promise.resolve(1),
Promise.reject(2),
Promise.resolve(3)
]).then(results => {
console.log(results);
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: 2 },
// { status: 'fulfilled', value: 3 }
// ]
});
8.6 async/await
async/await 是 Promise 的语法糖,让异步代码看起来像同步代码。
javascript
// 定义异步函数
async function fetchData() {
try {
const result = await promise; // 等待 Promise 完成
console.log(result);
// 可以 await 多次
const data1 = await fetch('/api/user').then(res => res.json());
const data2 = await fetch(`/api/user/${data1.id}`).then(res => res.json());
return data2;
} catch (error) {
console.error(error);
throw error; // 可以选择重新抛出错误
}
}
// 调用异步函数
fetchData()
.then(data => console.log(data))
.catch(error => console.error(error));
注意:
async函数返回一个 Promiseawait只能在async函数中使用await后面必须是一个 Promise 或 thenable 对象
8.7 Class 类
javascript
class Person {
// 静态属性
static species = '人类';
// 私有属性(# 开头)
#privateVar = '私有变量';
// 构造函数
constructor(name, age) {
this.name = name;
this.age = age;
}
// 实例方法
sayHello() {
console.log('Hello, I am', this.name);
console.log(this.#privateVar); // 可以访问私有属性
}
// Getter
get info() {
return `${this.name} is ${this.age} years old`;
}
// Setter
set age(newAge) {
if (newAge < 0) {
throw new Error('年龄不能为负数');
}
this._age = newAge;
}
// 静态方法
static getSpecies() {
return this.species;
}
}
const person = new Person('张三', 18);
person.sayHello(); // Hello, I am 张三
console.log(person.info); // 张三 is 18 years old
console.log(Person.getSpecies()); // 人类
// console.log(person.#privateVar); // ❌ 报错:无法访问私有属性
8.8 Module 模块化
javascript
// 导出(export)
// 文件:module.js
// 命名导出
export const name = '张三';
export const age = 18;
export function sayHello() {
console.log('Hello');
}
export class Person {}
// 默认导出(一个模块只能有一个默认导出)
export default function() {
console.log('默认导出');
}
// 导入(import)
// 文件:main.js
// 导入命名导出
import { name, age, sayHello } from './module.js';
// 导入默认导出
import myFunc from './module.js';
// 导入所有
import * as module from './module.js';
// 导入并重命名
import { name as userName } from './module.js';
九、函数相关
9.1 函数声明 vs 函数表达式
函数声明
javascript
function fn() {
console.log('函数声明');
}
fn();
- ✅ 会提升,可以在声明前调用
- ✅ 有 name 属性
函数表达式
javascript
const fn = function() {
console.log('函数表达式');
};
fn();
- ❌ 不会提升,必须先声明后调用
- ✅ 匿名函数,没有 name 属性(除非命名函数表达式)
命名函数表达式
javascript
const fn = function myFn() {
console.log('命名函数表达式');
};
console.log(fn.name); // 'myFn'
9.2 高阶函数
高阶函数是接收函数作为参数,或返回函数的函数。
javascript
// 接收函数作为参数
function map(arr, fn) {
const result = [];
for (const item of arr) {
result.push(fn(item));
}
return result;
}
const doubled = map([1, 2, 3], x => x * 2);
console.log(doubled); // [2, 4, 6]
// 返回函数
function createMultiplier(multiplier) {
return function(num) {
return num * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
9.3 柯里化
柯里化是将多参数函数转换为单参数函数序列的技术。
javascript
// 传统函数
function add(a, b, c) {
return a + b + c;
}
// 柯里化函数
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
// 箭头函数写法
const curriedAdd2 = a => b => c => a + b + c;
// 使用
console.log(curriedAdd(1)(2)(3)); // 6
// 部分应用(参数复用)
const add1 = curriedAdd(1);
console.log(add1(2)(3)); // 6
const add1And2 = add1(2);
console.log(add1And2(3)); // 6
9.4 防抖与节流
防抖
多次触发,只在最后一次触发后延迟执行。
javascript
function debounce(fn, delay) {
let timer = null;
return function(...args) {
// 清除之前的定时器
clearTimeout(timer);
// 设置新的定时器
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 使用场景:搜索框输入、窗口 resize
const search = debounce(function(keyword) {
console.log('搜索:', keyword);
}, 500);
search('a');
search('ab');
search('abc'); // 只执行最后一次
节流
固定时间间隔执行,无论触发多少次。
javascript
function throttle(fn, delay) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
lastTime = now;
fn.apply(this, args);
}
};
}
// 使用场景:滚动事件、鼠标移动
const scroll = throttle(function() {
console.log('滚动');
}, 1000);
window.addEventListener('scroll', scroll);
十、数组方法
10.1 改变原数组的方法
| 方法 | 说明 | 示例 |
|---|---|---|
| push() | 在末尾添加一个或多个元素 | arr.push(4) |
| pop() | 删除末尾的一个元素 | arr.pop() |
| shift() | 删除第一个元素 | arr.shift() |
| unshift() | 在开头添加一个或多个元素 | arr.unshift(0) |
| splice() | 删除、替换、插入元素 | arr.splice(1, 1, 'a') |
| sort() | 排序(默认按字符串排序) | arr.sort((a,b) => a-b) |
| reverse() | 反转数组 | arr.reverse() |
javascript
const arr = [1, 2, 3];
arr.push(4); // [1, 2, 3, 4]
arr.pop(); // [1, 2, 3]
arr.unshift(0); // [0, 1, 2, 3]
arr.shift(); // [1, 2, 3]
arr.splice(1, 1); // [1, 3](从索引 1 开始,删除 1 个元素)
// 排序
const nums = [3, 1, 4, 1, 5];
nums.sort((a, b) => a - b); // [1, 1, 3, 4, 5](升序)
nums.sort((a, b) => b - a); // [5, 4, 3, 1, 1](降序)
10.2 不改变原数组的方法
| 方法 | 说明 | 返回值 |
|---|---|---|
| slice() | 提取子数组 | 新数组 |
| concat() | 合并数组 | 新数组 |
| map() | 映射(转换每个元素) | 新数组 |
| filter() | 过滤(筛选符合条件的元素) | 新数组 |
| reduce() | 归约(累加器) | 单个值 |
| find() | 查找第一个符合条件的元素 | 元素或 undefined |
| findIndex() | 查找第一个符合条件的元素的索引 | 索引或 -1 |
| includes() | 判断是否包含某个元素 | 布尔值 |
| some() | 判断是否有元素满足条件 | 布尔值 |
| every() | 判断是否所有元素都满足条件 | 布尔值 |
| flatMap() | 先 map 再 flat(扁平化一层) | 新数组 |
javascript
const arr = [1, 2, 3, 4, 5];
// map:转换每个元素
const doubled = arr.map(x => x * 2); // [2, 4, 6, 8, 10]
// filter:过滤
const evens = arr.filter(x => x % 2 === 0); // [2, 4]
// find:查找
const found = arr.find(x => x > 3); // 4
// includes:判断包含
const has3 = arr.includes(3); // true
// some:任意满足
const hasEven = arr.some(x => x % 2 === 0); // true
// every:全部满足
const allPositive = arr.every(x => x > 0); // true
// flatMap
const nested = [[1], [2], [3]];
const flattened = nested.flatMap(x => x); // [1, 2, 3]
10.3 reduce() 详解
reduce() 是最强大的数组方法,可以实现几乎所有数组操作。
语法:
javascript
reduce((accumulator, currentValue, index, array) => {
// 返回新的累加值
}, initialValue);
参数:
accumulator:累加器,存储上次操作的结果currentValue:当前元素index:当前索引array:原数组initialValue:初始值(可选)
示例 1:数组求和
javascript
const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce((acc, cur) => acc + cur, 0);
console.log(sum); // 15
示例 2:数组去重
javascript
const arr = [1, 2, 2, 3, 3, 3];
const unique = arr.reduce((acc, cur) => {
return acc.includes(cur) ? acc : [...acc, cur];
}, []);
console.log(unique); // [1, 2, 3]
示例 3:对象属性统计
javascript
const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
const count = fruits.reduce((acc, cur) => {
acc[cur] = (acc[cur] || 0) + 1;
return acc;
}, {});
console.log(count); // { apple: 3, banana: 2, orange: 1 }
示例 4:数组转对象
javascript
const users = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' }
];
const userMap = users.reduce((acc, user) => {
acc[user.id] = user;
return acc;
}, {});
console.log(userMap);
// {
// 1: { id: 1, name: '张三' },
// 2: { id: 2, name: '李四' },
// 3: { id: 3, name: '王五' }
// }
示例 5:按属性分组
javascript
const users = [
{ name: '张三', age: 18 },
{ name: '李四', age: 20 },
{ name: '王五', age: 18 }
];
const grouped = users.reduce((acc, user) => {
const age = user.age;
if (!acc[age]) {
acc[age] = [];
}
acc[age].push(user);
return acc;
}, {});
console.log(grouped);
// {
// 18: [{ name: '张三', age: 18 }, { name: '王五', age: 18 }],
// 20: [{ name: '李四', age: 20 }]
// }
十一、对象与深浅拷贝
11.1 浅拷贝
浅拷贝只复制第一层属性,如果属性是引用类型,复制的是引用(地址)。
方法 1:Object.assign()
javascript
const obj1 = { name: '张三', hobbies: ['reading', 'coding'] };
const obj2 = Object.assign({}, obj1);
obj2.name = '李四';
obj2.hobbies.push('gaming');
console.log(obj1); // { name: '张三', hobbies: ['reading', 'coding', 'gaming'] }
console.log(obj2); // { name: '李四', hobbies: ['reading', 'coding', 'gaming'] }
// hobbies 是引用类型,被共享
方法 2:扩展运算符
javascript
const obj1 = { name: '张三', hobbies: ['reading', 'coding'] };
const obj2 = { ...obj1 };
// 结果同 Object.assign()
方法 3:Array.slice()(数组)
javascript
const arr1 = [1, { name: '张三' }];
const arr2 = arr1.slice();
arr2[0] = 2;
arr2[1].name = '李四';
console.log(arr1); // [1, { name: '李四' }]
console.log(arr2); // [2, { name: '李四' }]
11.2 深拷贝
深拷贝会递归复制所有层级的属性,完全独立。
方法 1:JSON 序列化(简单场景)
javascript
const obj1 = { name: '张三', hobbies: ['reading', 'coding'] };
const obj2 = JSON.parse(JSON.stringify(obj1));
obj2.name = '李四';
obj2.hobbies.push('gaming');
console.log(obj1); // { name: '张三', hobbies: ['reading', 'coding'] }
console.log(obj2); // { name: '李四', hobbies: ['reading', 'coding', 'gaming'] }
// ✅ 深拷贝成功
缺点:
- ❌ 无法处理
function、undefined、Symbol - ❌ 会丢失对象的原型链
- ❌ 循环引用会报错
javascript
const obj = {
fn: function() {},
undef: undefined,
sym: Symbol('id')
};
const copied = JSON.parse(JSON.stringify(obj));
console.log(copied); // {}(fn、undef、sym 都丢失了)
方法 2:递归深拷贝(推荐)
javascript
function deepClone(obj) {
// 如果是 null 或不是对象,直接返回
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 处理 Date 对象
if (obj instanceof Date) {
return new Date(obj);
}
// 处理 RegExp 对象
if (obj instanceof RegExp) {
return new RegExp(obj);
}
// 处理 Array 和 Object
const clone = Array.isArray(obj) ? [] : {};
for (const key in obj) {
// 只复制自身属性,不复制原型链上的属性
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]); // 递归调用
}
}
return clone;
}
// 测试
const obj1 = {
name: '张三',
hobbies: ['reading', 'coding'],
date: new Date(),
reg: /abc/g,
nested: { a: 1, b: { c: 2 } }
};
const obj2 = deepClone(obj1);
console.log(obj2); // 深拷贝成功
方法 3:使用第三方库
javascript
// lodash
const _ = require('lodash');
const obj2 = _.cloneDeep(obj1);
十二、错误处理
12.1 try-catch-finally
javascript
try {
// 可能出错的代码
const result = JSON.parse('invalid json');
} catch (error) {
// 捕获错误
console.error('错误类型:', error.constructor.name);
console.error('错误信息:', error.message);
console.error('错误堆栈:', error.stack);
} finally {
// 无论是否出错都会执行
console.log('finally 块');
}
12.2 throw 抛出错误
javascript
// 抛出内置错误
throw new Error('自定义错误');
throw new TypeError('类型错误');
throw new ReferenceError('引用错误');
throw new SyntaxError('语法错误');
throw new RangeError('范围错误');
// 抛出自定义错误
class CustomError extends Error {
constructor(message) {
super(message);
this.name = 'CustomError';
}
}
throw new CustomError('自定义错误');
12.3 常见错误类型
| 类型 | 说明 | 触发场景 |
|---|---|---|
| Error | 基类错误 | 一般错误 |
| TypeError | 类型错误 | 对 null/undefined 调用方法 |
| ReferenceError | 引用错误 | 使用未声明的变量 |
| SyntaxError | 语法错误 | 代码语法错误 |
| RangeError | 范围错误 | 数组长度超出范围 |
javascript
// TypeError
null.toString(); // TypeError: Cannot read property 'toString' of null
// ReferenceError
console.log(notDefined); // ReferenceError: notDefined is not defined
// SyntaxError
eval('var x = ;'); // SyntaxError: Unexpected token ;
// RangeError
new Array(-1); // RangeError: Invalid array length
十三、DOM 操作
13.1 获取元素
javascript
// 通过 ID 获取
document.getElementById('myId');
// 通过类名获取(返回 HTMLCollection)
document.getElementsByClassName('myClass');
// 通过标签名获取(返回 HTMLCollection)
document.getElementsByTagName('div');
// 通过选择器获取单个元素
document.querySelector('.myClass');
document.querySelector('#myId');
document.querySelector('div.active');
// 通过选择器获取多个元素(返回 NodeList)
document.querySelectorAll('.myClass');
document.querySelectorAll('div');
HTMLCollection vs NodeList:
- HTMLCollection:动态更新(DOM 变化时自动更新)
- NodeList:静态(获取时快照)
13.2 操作元素
javascript
const element = document.querySelector('#myElement');
// 修改文本
element.textContent = '文本内容';
element.innerHTML = '<strong>HTML 内容</strong>';
// 修改样式
element.style.color = 'red';
element.style.fontSize = '16px';
// 修改类名
element.className = 'active';
element.classList.add('active');
element.classList.remove('active');
element.classList.toggle('active');
element.classList.contains('active'); // true/false
// 修改属性
element.setAttribute('data-id', '123');
element.getAttribute('data-id');
element.removeAttribute('data-id');
13.3 创建和插入元素
javascript
// 创建元素
const div = document.createElement('div');
div.textContent = '新元素';
div.className = 'new-element';
// 插入元素
const parent = document.querySelector('#parent');
parent.appendChild(div); // 在末尾插入
// 在指定位置插入
const firstChild = parent.firstElementChild;
parent.insertBefore(div, firstChild); // 在 firstChild 之前插入
// 替换元素
const oldElement = document.querySelector('#oldElement');
parent.replaceChild(div, oldElement);
// 删除元素
parent.removeChild(div);
13.4 事件监听
javascript
const button = document.querySelector('button');
// 添加事件监听
button.addEventListener('click', function(event) {
console.log('点击事件');
console.log('事件对象:', event);
console.log('事件目标:', event.target);
console.log('当前元素:', event.currentTarget);
// 阻止事件冒泡
event.stopPropagation();
// 阻止默认行为
event.preventDefault();
});
// 只执行一次
button.addEventListener('click', fn, { once: true });
// 移除事件监听
function fn() {
console.log('事件');
}
button.addEventListener('click', fn);
button.removeEventListener('click', fn);
事件冒泡与捕获:
javascript
// 事件冒泡(从子元素到父元素)
div.addEventListener('click', fn, false); // false = 冒泡(默认)
// 事件捕获(从父元素到子元素)
div.addEventListener('click', fn, true); // true = 捕获
十四、BOM 操作
14.1 window 对象
javascript
// 获取窗口宽高
console.log(window.innerWidth); // 窗口内部宽度
console.log(window.innerHeight); // 窗口内部高度
console.log(window.outerWidth); // 窗口外部宽度
console.log(window.outerHeight); // 窗口外部高度
// 滚动到指定位置
window.scrollTo(0, 0);
window.scrollTo({ top: 0, behavior: 'smooth' });
// 打开新窗口
window.open('https://www.example.com', '_blank');
// 关闭窗口
window.close();
// 定时器
const timer1 = setTimeout(() => console.log('一次性定时器'), 1000);
const timer2 = setInterval(() => console.log('循环定时器'), 1000);
clearTimeout(timer1);
clearInterval(timer2);
14.2 location 对象
javascript
// 当前 URL 信息
console.log(location.href); // 完整 URL
console.log(location.protocol); // 协议(http:、https:)
console.log(location.host); // 主机名 + 端口
console.log(location.hostname); // 主机名
console.log(location.port); // 端口
console.log(location.pathname); // 路径
console.log(location.search); // 查询字符串(?name=value)
console.log(location.hash); // 哈希(#section)
// 跳转页面
location.href = 'https://www.example.com';
location.assign('https://www.example.com');
// 替换当前页面(无法后退)
location.replace('https://www.example.com');
// 刷新页面
location.reload();
14.3 history 对象
javascript
// 历史记录长度
console.log(history.length);
// 后退
history.back();
history.go(-1);
// 前进
history.forward();
history.go(1);
// 跳转
history.go(2); // 前进 2 步
history.go(-2); // 后退 2 步
14.4 本地存储
javascript
// localStorage(持久化存储)
localStorage.setItem('key', 'value');
const value = localStorage.getItem('key');
localStorage.removeItem('key');
localStorage.clear();
// sessionStorage(会话存储,关闭窗口后清除)
sessionStorage.setItem('key', 'value');
const value = sessionStorage.getItem('key');
sessionStorage.removeItem('key');
sessionStorage.clear();
// 注意:只能存储字符串
const obj = { name: '张三', age: 18 };
localStorage.setItem('user', JSON.stringify(obj)); // 存储
const user = JSON.parse(localStorage.getItem('user')); // 读取
localStorage vs sessionStorage:
| 特性 | localStorage | sessionStorage |
|---|---|---|
| 生命周期 | 持久化(手动清除) | 会话结束(关闭窗口) |
| 作用域 | 同源的所有窗口 | 当前窗口 |
| 容量 | 约 5MB | 约 5MB |
十五、性能优化
15.1 代码层面
1. 减少重排重绘
javascript
// ❌ 频繁修改 DOM(导致多次重排)
element.style.width = '100px';
element.style.height = '100px';
element.style.backgroundColor = 'red';
// ✅ 批量修改 DOM(只重排一次)
element.style.cssText = 'width: 100px; height: 100px; background-color: red;';
// ✅ 或者使用类名
element.className = 'my-class';
2. 使用事件委托
javascript
// ❌ 给每个元素绑定事件
const items = document.querySelectorAll('.item');
items.forEach(item => {
item.addEventListener('click', function() {
console.log('点击:', this.textContent);
});
});
// ✅ 使用事件委托(只绑定一次)
const container = document.querySelector('.container');
container.addEventListener('click', function(event) {
if (event.target.classList.contains('item')) {
console.log('点击:', event.target.textContent);
}
});
3. 避免内存泄漏
javascript
// ❌ 未清理事件监听
button.addEventListener('click', fn);
// 元素被移除后,事件监听依然存在,导致内存泄漏
// ✅ 清理事件监听
button.addEventListener('click', fn);
button.removeEventListener('click', fn);
// ✅ 清理定时器
const timer = setInterval(() => {}, 1000);
clearInterval(timer);
4. 使用防抖节流
见第八章「防抖与节流」
15.2 资源加载
1. 懒加载
javascript
// 图片懒加载
<img data-src="image.jpg" class="lazy" alt="图片">
const lazyImages = document.querySelectorAll('.lazy');
const lazyLoad = () => {
lazyImages.forEach(img => {
if (img.getBoundingClientRect().top <= window.innerHeight) {
img.src = img.dataset.src;
img.classList.remove('lazy');
}
});
};
window.addEventListener('scroll', lazyLoad);
window.addEventListener('load', lazyLoad);
2. 预加载
html
<!-- 预加载图片 -->
<link rel="preload" href="image.jpg" as="image">
<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="https://cdn.example.com">
15.3 渲染优化
1. 使用 requestAnimationFrame
javascript
// ❌ 使用 setTimeout
function animate() {
element.style.left = parseInt(element.style.left) + 1 + 'px';
setTimeout(animate, 16);
}
setTimeout(animate);
// ✅ 使用 requestAnimationFrame(与浏览器刷新率同步)
function animate() {
element.style.left = parseInt(element.style.left) + 1 + 'px';
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
2. 虚拟 DOM(React)
React 使用虚拟 DOM,最小化真实 DOM 操作。
3. SSR 服务端渲染
提高首屏加载速度,优化 SEO。