js

70 阅读20分钟

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 modeJSON、数组方法 map/filter/reduce)。
  • ES6 / ES2015

    • 重大更新:let/const、箭头函数、类(class)、模块(import/export)、Promise、解构赋值等。
  • ES2016~ES2023

    • 按年发布小版本更新,例如:

      • ES2017async/awaitObject.values/entries
      • ES2020: 可选链(?.)、空值合并(??)、BigInt
      • ES2022: 类静态块、顶层 await.at() 方法。
      • ES2023: 数组支持 findLasttoReversed 等新方法。

2. TC39 提案流程

新特性需通过 5 个阶段(Stage 0~4)才能纳入标准:

  • Stage 0(Strawman):初步想法。
  • Stage 4(Finished):确认纳入下一版标准。
  • 可通过 TC39 GitHub 跟踪提案进展。

3. 兼容性与工具

  • 浏览器/Node.js 支持

  • 编译工具

    • 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)。

注意事项

  1. 工具链:实际项目中直接用 Babel 转译 ES6+ 代码到 ES5。
  2. 性能:手动模拟可能影响性能(如箭头函数的 _this 闭包)。
  3. 局限性ProxySymbol 等特性无法完美降级。

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.jsrequire/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 APIAxios
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");
}

实际开发中的注意事项

  1. 避免在声明前使用变量:虽然不会报错,但会导致代码难以理解
  2. 使用 let/const 替代 var:避免意外的提升行为
  3. 函数表达式不会被提升
sayHi(); // 报错:sayHi is not a function
var sayHi = function() {
  console.log("Hi");
}
  1. 类声明不会被提升
const p = new Person(); // 报错
class Person {}

最佳实践

  1. 始终在使用前声明变量
  2. 优先使用 const,其次是 let,避免使用 var
  3. 将函数声明放在调用之前
  4. 使用 ESLint 等工具检测提升相关问题

闭包

什么是闭包?

闭包是指能够访问其他函数作用域中变量的函数,或者说函数与其相关的引用环境组合而成的实体。简单来说,当一个函数可以记住并访问所在的词法作用域,即使这个函数是在当前词法作用域之外执行,就产生了闭包。

闭包的基本原理

function outer() {
  const a = 10; // 外部函数变量
  
  function inner() { // 内部函数(闭包)
    console.log(a); // 访问外部函数变量
  }
  
  return inner;
}

const closureFunc = outer();
closureFunc(); // 输出:10

在这个例子中:

  1. inner 函数访问了 outer 函数的变量 a
  2. 即使 outer 已经执行完毕,inner 仍然可以访问 a
  3. closureFunc 就是一个闭包

闭包的特性

  1. 记忆性:闭包会记住创建时的环境
  2. 封装性:可以创建私有变量
  3. 持久性:闭包中的变量会一直存在,直到闭包被销毁

闭包的常见用途

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);
}

闭包的性能影响

  1. 优点

    • 实现数据封装和私有化
    • 保持变量状态
    • 实现高阶函数和函数式编程
  2. 缺点

    • 增加内存消耗(闭包中的变量不会被垃圾回收)
    • 过度使用可能导致性能问题

最佳实践

  1. 只在必要时使用闭包
  2. 及时释放不再需要的闭包(将闭包引用设为null)
  3. 避免在循环中创建不必要的闭包
  4. 使用模块化来组织闭包代码

事件冒泡

什么是事件冒泡?

事件冒泡是 DOM 事件传播的三种机制之一(另外两种是捕获和目标阶段),它描述的是当一个事件发生在某个 DOM 元素上时,该事件会从目标元素开始,向上传播("冒泡")到 DOM 树的根节点(document 对象)。

事件传播的三个阶段

  1. 捕获阶段(Capture Phase) :事件从 window 向下传播到目标元素
  2. 目标阶段(Target Phase) :事件到达目标元素
  3. 冒泡阶段(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

实际应用场景

  1. 模态框点击外部关闭
document.getElementById('modal').addEventListener('click', function(event) {
  event.stopPropagation();
});

document.addEventListener('click', function() {
  closeModal(); // 点击模态框外部时关闭
});
  1. 表格行点击处理
document.querySelector('table').addEventListener('click', function(event) {
  const row = event.target.closest('tr');
  if (row) {
    console.log('点击了行:', row.rowIndex);
  }
});

注意事项

  1. 不是所有事件都冒泡(如 focus、blur、load 等)
  2. event.stopPropagation() 会阻止所有父元素的事件处理程序执行
  3. event.stopImmediatePropagation() 还会阻止同一元素上的其他事件处理程序
  4. 过度使用事件阻止可能导致代码难以维护

最佳实践

  1. 优先使用事件委托处理大量相似元素的事件
  2. 谨慎使用 stopPropagation(),确保不会影响其他必要的事件处理
  3. 在复杂应用中,考虑使用事件命名空间或自定义事件系统

原型链&继承

原型链基础

  1. 原型对象(prototype)

每个函数都有一个 prototype 属性(箭头函数除外),指向该函数的原型对象。

function Person() {}
console.log(Person.prototype); // 输出原型对象
  1. proto 属性

每个对象(除 null 外)都有 proto 属性,指向创建该对象的构造函数的原型对象。

const person = new Person();
console.log(person.__proto__ === Person.prototype); // true
  1. 原型链查找机制

当访问对象属性时,JavaScript 会:

(1)先在对象自身查找

(2)找不到则通过 __proto__ 向上查找原型对象

(3)直到找到或到达原型链顶端(null)

Person.prototype.sayHello = function() {
  console.log("Hello!");
};

const p = new Person();
p.sayHello(); // 通过原型链找到方法

继承实现方式

  1. 原型链继承
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"

缺点

  • 所有子类实例共享父类引用属性
  • 无法向父类构造函数传参
  1. 构造函数继承
function Parent(name) {
  this.name = name;
}

function Child(name) {
  Parent.call(this, name); // 关键点
}

const c = new Child('Child');
console.log(c.name); // "Child"

优点

  • 解决引用属性共享问题
  • 可向父类传参

缺点

  • 无法继承父类原型上的方法
  1. 组合继承(最常用)
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中最常用的继承模式

缺点

  • 父类构造函数被调用两次
  1. 原型式继承(Object.create)
const parent = {
  name: 'Parent',
  sayName() {
    console.log(this.name);
  }
};

const child = Object.create(parent); // 关键点
child.name = 'Child';
child.sayName(); // "Child"
  1. 寄生组合式继承(最佳实践)
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 调用父类构造函数

关键概念

  1. constructor 属性

每个原型对象都有一个 constructor 属性,指向关联的构造函数。

function Person() {}
console.log(Person.prototype.constructor === Person); // true
  1. instanceof 原理

检查构造函数的 prototype 属性是否出现在对象的原型链上。

console.log(c instanceof Child); // true
console.log(c instanceof Parent); // true
console.log(c instanceof Object); // true
  1. 原型链终点

所有原型链的终点都是 Object.prototype,其 proto 为 null。

console.log(Object.prototype.__proto__); // null

最佳实践

  1. 现代开发优先使用 ES6 Class 语法
  2. 如需要兼容旧环境,使用寄生组合式继承
  3. 避免修改内置对象的原型(如 Array.prototype)
  4. 使用 Object.getPrototypeOf() 替代 __proto__(更规范)

微任务&宏任务&事件队列

基本概念

  1. 事件循环(Event Loop)机制 JavaScript 是单线程语言,通过事件循环机制实现异步操作。事件循环负责执行代码、收集和处理事件,以及执行队列中的子任务。

  2. 任务队列分类

  • 宏任务队列(Macrotask Queue/Task Queue) :包含整体 script、setTimeout、setInterval、I/O、UI渲染等
  • 微任务队列(Microtask Queue/Job Queue) :包含Promise.then、MutationObserver、process.nextTick(Node.js)等

执行顺序规则

  1. 同步代码最先执行
  2. 执行完同步代码后,检查微任务队列并执行所有微任务
  3. 执行一个宏任务
  4. 再次检查微任务队列并执行所有微任务
  5. 重复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与浏览器差异

  1. 浏览器环境
  • 每执行完一个宏任务就会清空微任务队列
  1. Node.js环境(11.x版本前后不同)
  • 11.x之前:阶段性地执行微任务
  • 11.x之后:与浏览器行为一致,每个宏任务后执行微任务

实际应用场景

  1. 优先级控制
// 确保某操作在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);
  }
}
  1. 批量操作优化
// 使用微任务批量处理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. 题目1
setTimeout(() => console.log('timeout'));

Promise.resolve()
  .then(() => console.log('promise'));

console.log('global');

// 输出顺序:
// global
// promise
// timeout
  1. 题目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
*/

最佳实践

  1. 耗时操作使用宏任务拆分,避免阻塞UI渲染
  2. 高优先级任务使用微任务确保尽快执行
  3. 避免在微任务中安排过多任务,可能导致页面卡顿
  4. 理解不同环境(Node.js/浏览器)下的差异

this指针

this 是 JavaScript 中一个非常重要但又容易令人困惑的概念,它的值取决于函数的调用方式,而不是定义位置。

this 的绑定规则

  1. 默认绑定(独立函数调用)

当函数作为普通函数调用时,this 指向全局对象(浏览器中是 window,Node.js 中是 global)。

function showThis() {
  console.log(this); // 浏览器中输出 window 对象
}
showThis(); // 独立函数调用

在严格模式('use strict')下,this 会是 undefined

  1. 隐式绑定(方法调用)

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

const obj = {
  name: 'Alice',
  greet: function() {
    console.log(`Hello, ${this.name}`);
  }
};
obj.greet(); // 输出 "Hello, Alice" - this 指向 obj
  1. 显式绑定(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"
  1. new 绑定(构造函数调用)

使用 new 调用构造函数时,this 指向新创建的对象。

function Person(name) {
  this.name = name;
}
const alice = new Person('Alice');
console.log(alice.name); // 输出 "Alice"
  1. 箭头函数中的 this

箭头函数没有自己的 this,它会捕获所在上下文的 this 值。

const obj = {
  name: 'Alice',
  greet: function() {
    setTimeout(() => {
      console.log(`Hello, ${this.name}`); // this 来自 greet 方法的 this
    }, 100);
  }
};
obj.greet(); // 输出 "Hello, Alice"

常见问题与陷阱

  1. 方法赋值给变量后调用
const obj = {
  name: 'Alice',
  greet: function() {
    console.log(this.name);
  }
};
const greetFunc = obj.greet;
greetFunc(); // 输出 undefined (this 指向全局对象或 undefined)
  1. 回调函数中的 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) 来查看当前值

超集

typescript