es6+语法
ES6(ECMAScript 2015)及后续版本引入了许多强大的新特性,下面分类介绍主要的语法特性:
1. 变量声明
- let/const:块级作用域变量声明
let x = 10;
const PI = 3.14;
2. 箭头函数
- 简洁的函数语法,不绑定自己的
this
const add = (a, b) => a + b;
const square = x => x * x;
3. 模板字符串
- 支持多行字符串和字符串插值
const name = 'Alice';
const greeting = `Hello, ${name}!
How are you today?`;
4. 解构赋值
- 从数组或对象中提取值
// 数组解构
const [first, second] = [1, 2, 3];
// 对象解构
const { name, age } = { name: 'Bob', age: 30 };
// 函数参数解构
function greet({ name, age }) {
return `Hello, ${name}! You are ${age}.`;
}
5. 扩展运算符与剩余参数
- 扩展运算符:展开数组或对象
const arr1 = [1, 2];
const arr2 = [...arr1, 3, 4]; // [1, 2, 3, 4]
const obj1 = { a: 1 };
const obj2 = { ...obj1, b: 2 }; // { a: 1, b: 2 }
- 剩余参数:收集多个参数为数组
function sum(...numbers) {
return numbers.reduce((a, b) => a + b);
}
6. 默认参数
- 函数参数默认值
function greet(name = 'Guest') {
return `Hello, ${name}!`;
}
7. 类语法
- 更直观的面向对象编程语法
class Person {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, ${this.name}!`;
}
static createAnonymous() {
return new Person('Anonymous');
}
}
class Student extends Person {
constructor(name, grade) {
super(name);
this.grade = grade;
}
}
8. 模块系统
- export/import:模块化开发
// math.js
export const PI = 3.14;
export function square(x) { return x * x; }
// app.js
import { PI, square } from './math.js';
import * as math from './math.js';
9. Promise 和异步编程
- Promise:处理异步操作
const fetchData = () => {
return new Promise((resolve, reject) => {
// 异步操作
if (success) {
resolve(data);
} else {
reject(error);
}
});
};
fetchData()
.then(data => console.log(data))
.catch(error => console.error(error));
- async/await(ES2017):更简洁的异步代码
async function getData() {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
console.error(error);
}
}
10. 对象字面量增强
- 简写属性和方法
const name = 'Alice';
const person = {
name, // 属性简写
greet() { // 方法简写
return `Hello, ${this.name}!`;
},
['id_' + Date.now()]: 123 // 计算属性名
};
11. 新的数据结构
- Map/Set
const map = new Map();
map.set('key', 'value');
const set = new Set([1, 2, 3]);
- WeakMap/WeakSet
12. 其他特性
- Symbol:唯一的值
const id = Symbol('id');
- for...of 循环
for (const item of iterable) {
console.log(item);
}
- Generator 函数
function* gen() {
yield 1;
yield 2;
}
- 可选链操作符
?.(ES2020)
const street = user?.address?.street;
- 空值合并操作符
??(ES2020)
const value = input ?? 'default';
- BigInt(ES2020)
const bigNum = 123456789012345678901234567890n;
- 动态导入(ES2020)
const module = await import('./module.js');
ES标准(深入理解js)
ECMAScript(简称 ES)是 JavaScript 语言的官方标准,由 ECMA International 组织制定(具体由 TC39 委员会 负责)。
1. 关键版本演进
-
ES5 (2009)
- 里程碑版本,广泛支持(如
strict mode、JSON、数组方法map/filter/reduce)。
- 里程碑版本,广泛支持(如
-
ES6 / ES2015
- 重大更新:
let/const、箭头函数、类(class)、模块(import/export)、Promise、解构赋值等。
- 重大更新:
-
ES2016~ES2023
-
按年发布小版本更新,例如:
- ES2017:
async/await、Object.values/entries。 - ES2020: 可选链(
?.)、空值合并(??)、BigInt。 - ES2022: 类静态块、顶层
await、.at()方法。 - ES2023: 数组支持
findLast、toReversed等新方法。
- ES2017:
-
2. TC39 提案流程
新特性需通过 5 个阶段(Stage 0~4)才能纳入标准:
- Stage 0(Strawman):初步想法。
- Stage 4(Finished):确认纳入下一版标准。
- 可通过 TC39 GitHub 跟踪提案进展。
3. 兼容性与工具
-
浏览器/Node.js 支持:
- 查看 Can I Use 或 Node.js ES Support。
-
编译工具:
- Babel:将新语法转译为旧版兼容代码。
- TypeScript:支持最新 ES 特性并提供类型检查。
4. 学习建议
- 基础:掌握 ES6+ 核心特性(如模块化、Promise、解构)。
- 跟进更新:关注年度发布的新特性(如 ES2023 的数组新方法)。
- 实践:通过现代框架(React/Vue)或 Node.js 应用新语法。
5. 示例代码(ES2020 特性)
// 可选链(Optional Chaining)
const userName = user?.profile?.name ?? '默认名字';
// 动态导入(Dynamic Import)
const module = await import('./module.js');
ES5实现ES6+语法糖
1. let/const → 用 var + 作用域模拟
ES6 的块级作用域在 ES5 中通过 IIFE(立即执行函数) 模拟:
// ES6
{
let x = 10;
const y = 20;
}
// ES5 模拟
(function() {
var x = 10;
var y = 20; // 通过函数作用域模拟块级作用域
})();
2. 箭头函数 → 普通函数 + bind(this)
箭头函数的 this 绑定需手动处理:
// ES6
const add = (a, b) => a + b;
// ES5
var add = function(a, b) {
return a + b;
};
// 带 this 的箭头函数
// ES6
obj.method = () => console.log(this);
// ES5
var _this = this;
obj.method = function() {
console.log(_this);
};
3. 类(class) → 构造函数 + 原型链
// ES6
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`Hi, ${this.name}`);
}
}
// ES5
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log('Hi, ' + this.name);
};
4. 模板字符串 → 字符串拼接
// ES6
const name = 'Alice';
console.log(`Hello, ${name}!`);
// ES5
var name = 'Alice';
console.log('Hello, ' + name + '!');
5. 解构赋值 → 逐个赋值
// ES6
const [a, b] = [1, 2];
const { x, y } = { x: 10, y: 20 };
// ES5
var arr = [1, 2];
var a = arr[0], b = arr[1];
var obj = { x: 10, y: 20 };
var x = obj.x, y = obj.y;
6. 默认参数 → 逻辑或(||)判断
// ES6
function greet(name = 'Guest') {
console.log('Hello, ' + name);
}
// ES5
function greet(name) {
name = name || 'Guest';
console.log('Hello, ' + name);
}
7. Promise → 回调函数或库(如 Bluebird)
ES5 原生不支持 Promise,需用回调或第三方库:
// ES6
new Promise((resolve) => resolve(123))
.then((val) => console.log(val));
// ES5 模拟(简化版)
function Promise(fn) {
var callbacks = [];
this.then = function(cb) {
callbacks.push(cb);
};
function resolve(val) {
callbacks.forEach(function(cb) {
cb(val);
});
}
fn(resolve);
}
8. 模块化(import/export)→ CommonJS/AMD
// ES6
import { util } from './module.js';
export const foo = 123;
// ES5 (CommonJS)
var util = require('./module.js').util;
exports.foo = 123; // 或 module.exports = { foo: 123 };
9. 剩余参数/展开运算符 → arguments 和 apply
// ES6
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
const arr = [1, 2, 3];
Math.max(...arr);
// ES5
function sum() {
var nums = Array.prototype.slice.call(arguments);
return nums.reduce(function(a, b) { return a + b; }, 0);
}
var arr = [1, 2, 3];
Math.max.apply(null, arr);
10. 生成器(Generator)→ 状态机或库
ES5 无法直接模拟 function*,需用复杂的状态机或库(如 Regenerator)。
注意事项
- 工具链:实际项目中直接用 Babel 转译 ES6+ 代码到 ES5。
- 性能:手动模拟可能影响性能(如箭头函数的
_this闭包)。 - 局限性:
Proxy、Symbol等特性无法完美降级。
js模块化
JavaScript 模块化是指将代码拆分为独立的模块(Module),每个模块具有明确的功能和依赖关系,以提高代码的可维护性、复用性和可测试性。随着 JavaScript 的发展,模块化方案经历了多个阶段:
1. 无模块化时代(全局变量污染)
早期 JavaScript 没有模块化,所有变量和函数都挂载在 window 对象上,容易造成命名冲突:
// moduleA.js
var name = 'Alice';
function sayHi() {
console.log('Hi, ' + name);
}
// moduleB.js
var name = 'Bob'; // 变量冲突!
function sayHi() {
console.log('Hello, ' + name);
}
2. 早期模块化方案
(1) IIFE(立即执行函数)
利用函数作用域隔离变量:
// moduleA.js
var moduleA = (function() {
var name = 'Alice';
function sayHi() {
console.log('Hi, ' + name);
}
return { sayHi }; // 暴露接口
})();
// moduleB.js
var moduleB = (function() {
var name = 'Bob';
function sayHi() {
console.log('Hello, ' + name);
}
return { sayHi };
})();
// 使用
moduleA.sayHi(); // "Hi, Alice"
moduleB.sayHi(); // "Hello, Bob"
(2) 命名空间模式
通过对象封装变量:
var MyApp = {};
MyApp.moduleA = {
name: 'Alice',
sayHi: function() {
console.log('Hi, ' + this.name);
}
};
3. 主流模块化规范
(1) CommonJS(Node.js 默认规范)
- 特点:同步加载,适用于服务器端(Node.js)。
- 语法:
// math.js
function add(a, b) { return a + b; }
module.exports = { add }; // 导出
// main.js
const math = require('./math.js'); // 导入
console.log(math.add(1, 2)); // 3
(2) AMD(异步模块定义,如 RequireJS)
- 特点:异步加载,适用于浏览器端。
- 语法:
// 定义模块
define(['dependency'], function(dep) {
return {
sayHi: function() { console.log('Hi'); }
};
});
// 加载模块
require(['module'], function(module) {
module.sayHi();
});
(3) CMD(通用模块定义,如 SeaJS)
类似 AMD,但更接近 CommonJS 的写法:
define(function(require, exports, module) {
var dep = require('dependency');
exports.sayHi = function() { console.log('Hi'); };
});
(4) UMD(通用模块定义)
兼容 CommonJS、AMD 和全局变量的混合模式:
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define(['dependency'], factory); // AMD
} else if (typeof module === 'object' && module.exports) {
module.exports = factory(require('dependency')); // CommonJS
} else {
root.MyModule = factory(root.dependency); // 全局变量
}
})(this, function(dep) {
return { sayHi: function() { console.log('Hi'); } };
});
4. ES Modules(ES6 原生模块化)
现代 JavaScript 的官方标准,浏览器和 Node.js 均支持:
(1) 基本语法
// math.js
export function add(a, b) { return a + b; }
export const PI = 3.14;
// main.js
import { add, PI } from './math.js';
console.log(add(1, 2)); // 3
(2) 默认导出
// utils.js
export default function log(msg) {
console.log(msg);
}
// main.js
import log from './utils.js';
log('Hello');
(3) 动态导入(Dynamic Import)
按需加载模块:
// 异步加载
import('./module.js').then(module => {
module.doSomething();
});
5. 模块化对比
| 方案 | 加载方式 | 适用环境 | 特点 |
|---|---|---|---|
| IIFE | 同步 | 浏览器 | 简单,但手动管理依赖 |
| CommonJS | 同步 | Node.js | require/module.exports |
| AMD | 异步 | 浏览器 | 适合动态加载(RequireJS) |
| ESM | 同步/异步 | 浏览器/Node.js | 官方标准,静态分析 |
6. 现代工具链
-
打包工具:Webpack、Rollup、Vite 支持多种模块化规范。
-
Node.js 支持 ESM:
- 在
package.json中添加"type": "module"。 - 或使用
.mjs后缀。
- 在
总结
- 浏览器端:优先使用 ES Modules(
<script type="module">)。 - Node.js:CommonJS 或 ESM(需配置)。
- 旧项目:AMD/UMD 兼容方案。
fetch api && ajax 和基于ajax封装的请求库
1. Ajax(XMLHttpRequest)
Ajax(Asynchronous JavaScript and XML)是一种使用 XMLHttpRequest(XHR)对象进行异步请求的技术。
特点
- 兼容性好:支持所有浏览器(包括 IE6+)。
- 回调方式:使用
onreadystatechange或onload监听请求状态。 - 手动处理数据:需手动解析
responseText或responseXML。
示例
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
console.log(data);
}
};
xhr.send();
缺点
- 代码冗长:需要手动处理
readyState和status。 - 回调地狱:多个请求嵌套时难以维护。
- 不支持 Promise:需手动封装。
2. Fetch API
Fetch API 是 ES6 引入的现代网络请求 API,基于 Promise,更简洁、强大。
特点
- Promise 风格:支持
.then()和async/await。 - 更简洁的语法:相比 XHR 代码更少。
- 默认不携带 Cookie:需手动设置
credentials: 'include'。 - 无法直接取消请求(需结合
AbortController)。 - 不处理 HTTP 错误状态(如 404、500 不会 reject,需手动检查
response.ok)。
示例
// GET 请求
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) throw new Error('Network error');
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error(error));
// POST 请求
fetch('https://api.example.com/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'value' }),
})
.then(response => response.json())
.then(data => console.log(data));
缺点
- 不支持超时设置(需结合
AbortController)。 - 兼容性问题:IE 完全不支持(需 polyfill)。
- 不自动处理错误状态码(需手动检查
response.ok)。
3. 基于 Ajax 封装的请求库
由于原生 Ajax 和 Fetch API 仍有一些不足,社区出现了许多封装库,如:
- Axios(最流行)
- jQuery.ajax(旧项目常见)
- SuperAgent(Node.js & 浏览器)
- Got(Node.js)
Axios(推荐)
特点
- 基于 Promise:支持
async/await。 - 自动转换 JSON:无需手动
response.json()。 - 拦截器:可全局拦截请求和响应。
- 取消请求:支持
CancelToken(旧版)或AbortController(新版)。 - 浏览器 & Node.js:同一套 API 兼容两端。
示例
// GET 请求
axios.get('https://api.example.com/data')
.then(response => console.log(response.data))
.catch(error => console.error(error));
// POST 请求
axios.post('https://api.example.com/data', { key: 'value' })
.then(response => console.log(response.data));
// 拦截器
axios.interceptors.request.use(config => {
config.headers.Authorization = 'Bearer token';
return config;
});
// 取消请求
const controller = new AbortController();
axios.get('https://api.example.com/data', {
signal: controller.signal,
}).catch(error => {
if (axios.isCancel(error)) console.log('Request canceled');
});
controller.abort(); // 取消请求
对比 Fetch API 的优势
| 特性 | Fetch API | Axios |
|---|---|---|
| JSON 自动转换 | ❌ 需手动 response.json() | ✅ 自动解析 data |
| 错误处理 | ❌ 不 reject HTTP 错误 | ✅ 自动 reject 非 2xx 响应 |
| 取消请求 | ❌ 需 AbortController | ✅ 支持 CancelToken/AbortController |
| 拦截器 | ❌ 不支持 | ✅ 全局请求/响应拦截 |
| 浏览器兼容性 | ❌ 不兼容 IE | ✅ 兼容 IE11+(需 polyfill) |
4. 如何选择?
| 场景 | 推荐方案 |
|---|---|
| 现代浏览器项目 | Fetch API(原生,无需额外库) |
| 需要完整功能/兼容性 | Axios(拦截器、取消请求、错误处理) |
| 旧项目(jQuery) | jQuery.ajax |
| Node.js 环境 | Axios 或 Got |
总结
- Fetch API:现代、轻量,适合新项目,但需手动处理部分逻辑。
- Axios:功能全面,推荐大多数项目使用。
- 原生 Ajax:仅用于兼容极端旧环境。
一些概念
变量提升
什么是变量提升?
变量提升(Hoisting)是 JavaScript 中的一个重要机制,它指的是在代码执行前,JavaScript 引擎会将变量和函数的声明提升到它们所在作用域的顶部。这意味着你可以在声明之前使用变量或函数,而不会报错。
变量提升的规则
1. var 变量的提升
console.log(a); // 输出:undefined
var a = 10;
console.log(a); // 输出:10
等价于:
var a; // 声明被提升
console.log(a); // 输出:undefined
a = 10; // 赋值留在原地
console.log(a); // 输出:10
2. 函数声明的提升
sayHello(); // 输出:"Hello!"
function sayHello() {
console.log("Hello!");
}
等价于:
function sayHello() { // 整个函数声明被提升
console.log("Hello!");
}
sayHello(); // 输出:"Hello!"
3. let 和 const 的提升(TDZ)
console.log(b); // 报错:Cannot access 'b' before initialization
let b = 20;
let 和 const 也有提升,但存在暂时性死区(Temporal Dead Zone, TDZ),在声明前访问会报错。
提升的优先级
1. 函数声明 > 变量声明
console.log(foo); // 输出函数定义
function foo() {}
var foo = 10;
2. 后声明的函数会覆盖前面的
foo(); // 输出:"Second"
function foo() {
console.log("First");
}
function foo() {
console.log("Second");
}
实际开发中的注意事项
- 避免在声明前使用变量:虽然不会报错,但会导致代码难以理解
- 使用 let/const 替代 var:避免意外的提升行为
- 函数表达式不会被提升
sayHi(); // 报错:sayHi is not a function
var sayHi = function() {
console.log("Hi");
}
- 类声明不会被提升
const p = new Person(); // 报错
class Person {}
最佳实践
- 始终在使用前声明变量
- 优先使用 const,其次是 let,避免使用 var
- 将函数声明放在调用之前
- 使用 ESLint 等工具检测提升相关问题
闭包
什么是闭包?
闭包是指能够访问其他函数作用域中变量的函数,或者说函数与其相关的引用环境组合而成的实体。简单来说,当一个函数可以记住并访问所在的词法作用域,即使这个函数是在当前词法作用域之外执行,就产生了闭包。
闭包的基本原理
function outer() {
const a = 10; // 外部函数变量
function inner() { // 内部函数(闭包)
console.log(a); // 访问外部函数变量
}
return inner;
}
const closureFunc = outer();
closureFunc(); // 输出:10
在这个例子中:
inner函数访问了outer函数的变量a- 即使
outer已经执行完毕,inner仍然可以访问a closureFunc就是一个闭包
闭包的特性
- 记忆性:闭包会记住创建时的环境
- 封装性:可以创建私有变量
- 持久性:闭包中的变量会一直存在,直到闭包被销毁
闭包的常见用途
1. 创建私有变量
function createCounter() {
let count = 0; // 私有变量
return {
increment: function() { count++; },
decrement: function() { count--; },
getCount: function() { return count; }
};
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount()); // 2
console.log(count); // 报错:count is not defined
2. 实现函数柯里化
function multiply(a) {
return function(b) {
return a * b;
};
}
const double = multiply(2);
console.log(double(5)); // 10
3. 模块模式
const calculator = (function() {
let result = 0;
return {
add: function(x) { result += x; },
subtract: function(x) { result -= x; },
getResult: function() { return result; }
};
})();
calculator.add(10);
calculator.subtract(5);
console.log(calculator.getResult()); // 5
闭包的注意事项
1. 内存泄漏风险:
function createHeavyObject() {
const bigArray = new Array(1000000).fill('*');
return function() {
console.log(bigArray.length);
};
}
const heavyClosure = createHeavyObject();
// bigArray 不会被释放,直到 heavyClosure 不再被引用
2. 循环中的闭包问题:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // 输出5个5,而不是0,1,2,3,4
}, 100);
}
// 解决方案1:使用let
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // 正确输出0,1,2,3,4
}, 100);
}
// 解决方案2:IIFE
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 正确输出0,1,2,3,4
}, 100);
})(i);
}
闭包的性能影响
-
优点:
- 实现数据封装和私有化
- 保持变量状态
- 实现高阶函数和函数式编程
-
缺点:
- 增加内存消耗(闭包中的变量不会被垃圾回收)
- 过度使用可能导致性能问题
最佳实践
- 只在必要时使用闭包
- 及时释放不再需要的闭包(将闭包引用设为null)
- 避免在循环中创建不必要的闭包
- 使用模块化来组织闭包代码
事件冒泡
什么是事件冒泡?
事件冒泡是 DOM 事件传播的三种机制之一(另外两种是捕获和目标阶段),它描述的是当一个事件发生在某个 DOM 元素上时,该事件会从目标元素开始,向上传播("冒泡")到 DOM 树的根节点(document 对象)。
事件传播的三个阶段
- 捕获阶段(Capture Phase) :事件从 window 向下传播到目标元素
- 目标阶段(Target Phase) :事件到达目标元素
- 冒泡阶段(Bubble Phase) :事件从目标元素向上传播回 window
<div id="grandparent">
<div id="parent">
<div id="child">点击我</div>
</div>
</div>
document.getElementById('grandparent').addEventListener('click', function() {
console.log('Grandparent clicked');
}, false); // 冒泡阶段触发(默认)
document.getElementById('parent').addEventListener('click', function() {
console.log('Parent clicked');
}, false);
document.getElementById('child').addEventListener('click', function() {
console.log('Child clicked');
}, false);
点击 child 元素时,控制台输出顺序:
Child clicked
Parent clicked
Grandparent clicked
阻止事件冒泡
使用 event.stopPropagation() 方法可以阻止事件继续向上冒泡:
document.getElementById('child').addEventListener('click', function(event) {
console.log('Child clicked');
event.stopPropagation(); // 阻止事件冒泡
}, false);
现在点击 child 元素只会输出:
Child clicked
事件委托(Event Delegation)
利用事件冒泡机制可以实现事件委托,这是一种优化事件处理的技术:
<ul id="list">
<li>项目1</li>
<li>项目2</li>
<li>项目3</li>
</ul>
document.getElementById('list').addEventListener('click', function(event) {
if (event.target.tagName === 'LI') {
console.log('点击了:', event.target.textContent);
}
});
优点:
- 减少事件处理程序数量(内存占用更少)
- 动态添加的子元素自动拥有事件处理能力
冒泡与捕获的区别
// 捕获阶段(第三个参数为 true)
document.getElementById('grandparent').addEventListener('click', function() {
console.log('Grandparent capture');
}, true);
// 冒泡阶段(第三个参数为 false 或省略)
document.getElementById('grandparent').addEventListener('click', function() {
console.log('Grandparent bubble');
}, false);
点击 child 元素时的输出顺序:
Grandparent capture
Child clicked
Grandparent bubble
实际应用场景
- 模态框点击外部关闭:
document.getElementById('modal').addEventListener('click', function(event) {
event.stopPropagation();
});
document.addEventListener('click', function() {
closeModal(); // 点击模态框外部时关闭
});
- 表格行点击处理:
document.querySelector('table').addEventListener('click', function(event) {
const row = event.target.closest('tr');
if (row) {
console.log('点击了行:', row.rowIndex);
}
});
注意事项
- 不是所有事件都冒泡(如 focus、blur、load 等)
event.stopPropagation()会阻止所有父元素的事件处理程序执行event.stopImmediatePropagation()还会阻止同一元素上的其他事件处理程序- 过度使用事件阻止可能导致代码难以维护
最佳实践
- 优先使用事件委托处理大量相似元素的事件
- 谨慎使用
stopPropagation(),确保不会影响其他必要的事件处理 - 在复杂应用中,考虑使用事件命名空间或自定义事件系统
原型链&继承
原型链基础
- 原型对象(prototype)
每个函数都有一个 prototype 属性(箭头函数除外),指向该函数的原型对象。
function Person() {}
console.log(Person.prototype); // 输出原型对象
- proto 属性
每个对象(除 null 外)都有 proto 属性,指向创建该对象的构造函数的原型对象。
const person = new Person();
console.log(person.__proto__ === Person.prototype); // true
- 原型链查找机制
当访问对象属性时,JavaScript 会:
(1)先在对象自身查找
(2)找不到则通过 __proto__ 向上查找原型对象
(3)直到找到或到达原型链顶端(null)
Person.prototype.sayHello = function() {
console.log("Hello!");
};
const p = new Person();
p.sayHello(); // 通过原型链找到方法
继承实现方式
- 原型链继承
function Parent() {
this.name = 'Parent';
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child() {}
Child.prototype = new Parent(); // 关键点
const c = new Child();
c.sayName(); // "Parent"
缺点:
- 所有子类实例共享父类引用属性
- 无法向父类构造函数传参
- 构造函数继承
function Parent(name) {
this.name = name;
}
function Child(name) {
Parent.call(this, name); // 关键点
}
const c = new Child('Child');
console.log(c.name); // "Child"
优点:
- 解决引用属性共享问题
- 可向父类传参
缺点:
- 无法继承父类原型上的方法
- 组合继承(最常用)
function Parent(name) {
this.name = name;
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name); // 第二次调用Parent
this.age = age;
}
Child.prototype = new Parent(); // 第一次调用Parent
Child.prototype.constructor = Child; // 修复constructor
const c = new Child('Tom', 10);
c.sayName(); // "Tom"
优点:
- 结合两种继承方式的优点
- 是JavaScript中最常用的继承模式
缺点:
- 父类构造函数被调用两次
- 原型式继承(Object.create)
const parent = {
name: 'Parent',
sayName() {
console.log(this.name);
}
};
const child = Object.create(parent); // 关键点
child.name = 'Child';
child.sayName(); // "Child"
- 寄生组合式继承(最佳实践)
function inheritPrototype(child, parent) {
const prototype = Object.create(parent.prototype); // 创建对象
prototype.constructor = child; // 增强对象
child.prototype = prototype; // 指定对象
}
function Parent(name) {
this.name = name;
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
inheritPrototype(Child, Parent); // 实现继承
const c = new Child('Tom', 10);
c.sayName(); // "Tom"
优点:
- 只调用一次父类构造函数
- 保持原型链完整
- 最高效的继承方式
ES6 Class 继承
class Parent {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class Child extends Parent { // 关键点:extends
constructor(name, age) {
super(name); // 关键点:super
this.age = age;
}
sayAge() {
console.log(this.age);
}
}
const c = new Child('Tom', 10);
c.sayName(); // "Tom"
c.sayAge(); // 10
特点:
- 语法糖,底层仍是原型继承
extends实现继承super调用父类构造函数
关键概念
- constructor 属性
每个原型对象都有一个 constructor 属性,指向关联的构造函数。
function Person() {}
console.log(Person.prototype.constructor === Person); // true
- instanceof 原理
检查构造函数的 prototype 属性是否出现在对象的原型链上。
console.log(c instanceof Child); // true
console.log(c instanceof Parent); // true
console.log(c instanceof Object); // true
- 原型链终点
所有原型链的终点都是 Object.prototype,其 proto 为 null。
console.log(Object.prototype.__proto__); // null
最佳实践
- 现代开发优先使用 ES6 Class 语法
- 如需要兼容旧环境,使用寄生组合式继承
- 避免修改内置对象的原型(如 Array.prototype)
- 使用
Object.getPrototypeOf()替代__proto__(更规范)
微任务&宏任务&事件队列
基本概念
-
事件循环(Event Loop)机制 JavaScript 是单线程语言,通过事件循环机制实现异步操作。事件循环负责执行代码、收集和处理事件,以及执行队列中的子任务。
-
任务队列分类
- 宏任务队列(Macrotask Queue/Task Queue) :包含整体 script、setTimeout、setInterval、I/O、UI渲染等
- 微任务队列(Microtask Queue/Job Queue) :包含Promise.then、MutationObserver、process.nextTick(Node.js)等
执行顺序规则
- 同步代码最先执行
- 执行完同步代码后,检查微任务队列并执行所有微任务
- 执行一个宏任务
- 再次检查微任务队列并执行所有微任务
- 重复3-4步骤
console.log('1. 同步代码开始');
setTimeout(() => {
console.log('6. setTimeout宏任务');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise微任务1');
}).then(() => {
console.log('4. Promise微任务2');
});
console.log('2. 同步代码结束');
/* 输出顺序:
1. 同步代码开始
2. 同步代码结束
3. Promise微任务1
4. Promise微任务2
6. setTimeout宏任务
*/
常见任务分类
宏任务(Macrotasks)
<script>整体代码- setTimeout/setInterval
- setImmediate(Node.js)
- I/O操作
- UI渲染
- 事件回调(如click事件)
微任务(Microtasks)
- Promise.then/catch/finally
- MutationObserver
- process.nextTick(Node.js)
- queueMicrotask API
复杂示例分析
console.log('1. 同步代码');
setTimeout(() => {
console.log('6. setTimeout1');
Promise.resolve().then(() => {
console.log('7. Promise in setTimeout');
});
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise1');
setTimeout(() => {
console.log('8. setTimeout in Promise');
}, 0);
});
Promise.resolve().then(() => {
console.log('4. Promise2');
});
setTimeout(() => {
console.log('5. setTimeout2');
}, 0);
console.log('2. 同步代码结束');
/* 输出顺序:
1. 同步代码
2. 同步代码结束
3. Promise1
4. Promise2
5. setTimeout2
6. setTimeout1
7. Promise in setTimeout
8. setTimeout in Promise
*/
Node.js与浏览器差异
- 浏览器环境
- 每执行完一个宏任务就会清空微任务队列
- Node.js环境(11.x版本前后不同)
- 11.x之前:阶段性地执行微任务
- 11.x之后:与浏览器行为一致,每个宏任务后执行微任务
实际应用场景
- 优先级控制
// 确保某操作在DOM更新后执行
function nextTick(callback) {
if (typeof Promise !== 'undefined') {
Promise.resolve().then(callback);
} else if (typeof MutationObserver !== 'undefined') {
const observer = new MutationObserver(callback);
const textNode = document.createTextNode('1');
observer.observe(textNode, { characterData: true });
textNode.data = '2';
} else {
setTimeout(callback, 0);
}
}
- 批量操作优化
// 使用微任务批量处理UI更新
let isUpdating = false;
const queue = [];
function updateUI() {
if (isUpdating) return;
isUpdating = true;
queueMicrotask(() => {
// 批量处理队列中的所有更新
while (queue.length) {
const task = queue.shift();
task();
}
isUpdating = false;
});
}
常见面试题解析
- 题目1
setTimeout(() => console.log('timeout'));
Promise.resolve()
.then(() => console.log('promise'));
console.log('global');
// 输出顺序:
// global
// promise
// timeout
- 题目2
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
new Promise((resolve) => {
console.log('4');
resolve();
}).then(() => {
console.log('5');
setTimeout(() => {
console.log('6');
}, 0);
});
console.log('7');
/* 输出顺序:
1
4
7
5
2
3
6
*/
最佳实践
- 耗时操作使用宏任务拆分,避免阻塞UI渲染
- 高优先级任务使用微任务确保尽快执行
- 避免在微任务中安排过多任务,可能导致页面卡顿
- 理解不同环境(Node.js/浏览器)下的差异
this指针
this 是 JavaScript 中一个非常重要但又容易令人困惑的概念,它的值取决于函数的调用方式,而不是定义位置。
this 的绑定规则
- 默认绑定(独立函数调用)
当函数作为普通函数调用时,this 指向全局对象(浏览器中是 window,Node.js 中是 global)。
function showThis() {
console.log(this); // 浏览器中输出 window 对象
}
showThis(); // 独立函数调用
在严格模式('use strict')下,this 会是 undefined。
- 隐式绑定(方法调用)
当函数作为对象的方法调用时,this 指向调用该方法的对象。
const obj = {
name: 'Alice',
greet: function() {
console.log(`Hello, ${this.name}`);
}
};
obj.greet(); // 输出 "Hello, Alice" - this 指向 obj
- 显式绑定(call, apply, bind)
可以使用 call(), apply() 或 bind() 方法显式设置 this 的值。
function greet() {
console.log(`Hello, ${this.name}`);
}
const person = { name: 'Bob' };
greet.call(person); // 输出 "Hello, Bob"
greet.apply(person); // 输出 "Hello, Bob"
const boundGreet = greet.bind(person);
boundGreet(); // 输出 "Hello, Bob"
- new 绑定(构造函数调用)
使用 new 调用构造函数时,this 指向新创建的对象。
function Person(name) {
this.name = name;
}
const alice = new Person('Alice');
console.log(alice.name); // 输出 "Alice"
- 箭头函数中的 this
箭头函数没有自己的 this,它会捕获所在上下文的 this 值。
const obj = {
name: 'Alice',
greet: function() {
setTimeout(() => {
console.log(`Hello, ${this.name}`); // this 来自 greet 方法的 this
}, 100);
}
};
obj.greet(); // 输出 "Hello, Alice"
常见问题与陷阱
- 方法赋值给变量后调用:
const obj = {
name: 'Alice',
greet: function() {
console.log(this.name);
}
};
const greetFunc = obj.greet;
greetFunc(); // 输出 undefined (this 指向全局对象或 undefined)
- 回调函数中的 this:
const obj = {
name: 'Alice',
greet: function() {
setTimeout(function() {
console.log(this.name); // 错误!this 指向全局对象或 undefined
}, 100);
}
};
obj.greet();
解决方案是使用箭头函数或 bind:
// 使用箭头函数
setTimeout(() => {
console.log(this.name);
}, 100);
// 或使用 bind
setTimeout(function() {
console.log(this.name);
}.bind(this), 100);
总结
this的值取决于函数的调用方式- 四种绑定规则:默认绑定、隐式绑定、显式绑定、new 绑定
- 箭头函数不绑定自己的
this,而是继承外层作用域的this - 当不确定
this指向时,可以使用console.log(this)来查看当前值