Skip to content

JavaScript

数据类型

JavaScript 中有两类数据类型:基本数据类型(Primitive Types)和对象类型(Object Types)。

基本数据类型

JavaScript 有 7 种基本数据类型,分别是:

  • undefined(未定义) 表示变量未赋值时的默认值。
  • null(空值) 表示变量的值为空或不存在。
  • boolean(布尔值) 表示逻辑上的真或假。可以是 truefalse
  • number(数字) 表示整数或浮点数。JavaScript 中的所有数字都是 64 位浮点数。
  • string(字符串) 表示文本数据。可以使用单引号(')或双引号(")来定义字符串。
  • symbol(符号) ES6(ECMAScript 2015)中的新类型,表示独一无二的值。
  • bigint(大整数) ES2020 中的新类型,用于表示任意精度的整数。

对象类型

对象类型是可变的,用于存储和组织复杂的数据和功能。JavaScript 中的对象是键值对的集合,可以包含属性和方法。

  • Object 通用的对象类型,可以包含属性和方法。
  • Array 表示有序的集合,每个元素可以通过索引访问。
  • Function 表示可调用的代码块,可以接受参数并返回值。
  • Date 表示日期和时间。
  • RegExp 表示正则表达式,用于字符串匹配。
  • 其他内置对象,如 MapSetPromise 等,提供了各种功能和数据结构。

JavaScript 是一种动态类型语言,变量在运行时可以被赋予不同的数据类型。此外,JavaScript 还具有一些特殊的值,如 InfinityNaN 等,用于表示特殊的数值情况。

堆(Heap)和栈(Stack)

在 JavaScript 中,内存主要分为 堆(Heap)栈(Stack) 两部分,它们用于存储不同类型的数据,并决定了数据的访问方式和生命周期。

堆(Heap):存放复杂数据的“仓库”

  • 存储内容:对象(Object)、数组(Array)等复杂数据类型。
  • 访问方式:通过引用访问,而不是直接存取数据。
  • 管理方式:JS 引擎的垃圾回收机制自动管理,不用手动释放。
  • 特点:数据存储不连续,查找速度比栈慢,但能存储更大、更复杂的数据。

栈(Stack):存放简单数据的“快递柜”

  • 存储内容:基本数据类型(Number、String、Boolean、Null、Undefined、Symbol)。
  • 访问方式:按存取,访问速度快。
  • 管理方式先进后出(LIFO),函数执行完毕后,相关数据会自动释放。
  • 特点:存储数据简单、读取快、生命周期短。

关键对比

特性堆(Heap)栈(Stack)
存储类型复杂数据(对象、数组)基本数据(数字、字符串等)
存取方式通过引用访问直接存取
管理方式由垃圾回收机制管理先进后出(LIFO),自动释放
访问速度相对较慢速度快

基本数据类型和对象类型的区别

存储方式

  • 基本数据类型:直接存储在内存(Stack)中,保存的是实际值
    • 包括:numberstringbooleannullundefinedSymbol(ES6)、BigInt(ES11)。
  • 对象类型:值存储在内存(Heap)中,栈内存中保存的是堆内存的引用地址
    • 包括:ObjectArrayFunctionDateRegExp 等。

可变性

  • 基本数据类型不可变(Immutable)。例如修改字符串会创建新值,而非修改原值:
    javascript
    let str = "hello";
    str[0] = "H"; // 无效,str 仍为 "hello"
  • 对象类型可变(Mutable)。可以直接修改属性:
    javascript
    let obj = { x: 1 };
    obj.x = 2; // 修改成功

比较方式

  • 基本数据类型:比较是否相等。
    javascript
    5 === 5; // true
    "abc" === "abc"; // true
  • 对象类型:比较引用地址是否指向同一内存。
    javascript
    {} === {}; // false(两个独立对象)
    let a = {};
    let b = a;
    a === b; // true(引用相同)

复制行为

  • 基本数据类型:赋值时复制值本身(深拷贝)。
    javascript
    let a = 5;
    let b = a; // b 是 a 的副本
  • 对象类型:赋值时复制引用地址(浅拷贝)。
    javascript
    let obj1 = { x: 1 };
    let obj2 = obj1; // obj2 与 obj1 共享同一内存

包装对象(Wrapper Object)

  • 基本数据类型:调用方法(如 str.toUpperCase())时,JS 会临时将其包装为对应对象(如 String),调用后立即销毁。
  • 对象类型:无需包装,可直接操作。

类型检测

  • 基本数据类型:通过 typeof 检测(注意 typeof null === "object" 是历史遗留问题)。
    javascript
    typeof 42; // "number"
    typeof "abc"; // "string"
    typeof undefined; // "undefined"
  • 对象类型
    • typeof {}"object"
    • typeof []"object"
    • typeof function() {}"function"
    • 使用 instanceofObject.prototype.toString 进一步判断:
      javascript
      [] instanceof Array; // true
      Object.prototype.toString.call([]); // "[object Array]"

内存管理

  • 基本数据类型:占用固定内存,生命周期随作用域结束自动回收。
  • 对象类型:动态分配内存,通过垃圾回收机制(GC)管理,需解除所有引用才能回收。

总结

特性基本数据类型对象类型
存储方式栈内存(直接存值)值存储在堆内存(栈存引用地址)
可变性不可变可变
比较方式值相等引用地址相等
复制行为深拷贝(复制值)浅拷贝(复制引用)
内存占用固定且轻量动态且可能较大
类型检测typeof(注意 nullinstanceof/Object.prototype.toString

内置对象

JavaScript 中的内置对象是指在语言核心中已经定义好的对象,开发者可以直接使用它们而无需额外的定义。这些对象提供了各种功能和数据结构,以帮助开发者更方便地进行编程。

以下是一些常见的 JavaScript 内置对象:

  • Object(对象): 是所有对象的基础。其他对象都继承自 Object。对象是键值对的集合,用于存储和组织数据。

    javascript
    var person = { name: 'John', age: 30 };
  • Array(数组): 表示有序的集合,每个元素可以通过索引访问。提供了丰富的方法用于操作数组。

    javascript
    var numbers = [1, 2, 3, 4, 5];
  • Function(函数): 函数是一等公民,可以被赋值给变量,传递给其他函数,从其他函数返回。函数用于执行可重复使用的代码块。

    javascript
    function add(a, b) {
      return a + b;
    }
  • String(字符串): 表示文本数据。提供了许多字符串操作方法,如拼接、截取、查找等。

    javascript
    var message = 'Hello, World!';
  • Number(数字): 表示数字。提供了一些数学运算和常量,如Math.PI

    javascript
    var count = 42;
  • Boolean(布尔值): 表示逻辑上的真或假。只有两个值:truefalse

    javascript
    var isTrue = true;
  • Date(日期): 用于处理日期和时间。提供了许多方法和选项来操作日期对象。

    javascript
    var today = new Date();
  • RegExp(正则表达式): 用于字符串模式匹配。提供了一种强大的方式来搜索、替换和提取字符串中的模式。

    javascript
    var pattern = /\d+/;
  • Math(数学): 提供了数学相关的方法和常量,如三角函数、对数、指数等。

    javascript
    var result = Math.sqrt(25);
  • Error(错误): 是一个基础的错误对象,用于表示运行时错误。其他错误对象(如SyntaxErrorTypeError)都继承自它。

    javascript
    throw new Error('Something went wrong');

此外,还有其他一些内置对象,如MapSetPromise等,它们提供了更高级的数据结构和异步编程的支持。

let 和 const

letconst 是 ES6(ECMAScript 2015)引入的两个用于声明变量的关键字,相较于传统的 var,它们在作用域、变量重新赋值和暂时性死区等方面有一些不同之处。

let(变量)

  • 块级作用域(仅在 {} 内有效)。
  • 可重新赋值,但不会提升到全局作用域。
js
let count = 0;
count = 1; // 可修改
console.log(count); // 1
js
if (true) {
  let x = 10;
}
console.log(x); // 报错(未定义)

const(常量)

  • 块级作用域必须赋初值
  • 不可重新赋值,但对象、数组内部可修改。
js
const pi = 3.14;
pi = 4; // 报错(不可修改)
js
const person = { name: 'John' };
person.name = 'Jane'; // 可修改对象属性
person = {}; // 报错(不能重新赋值)

区别总结

关键点letconst
作用域块级作用域块级作用域
重新赋值可以不可
变量提升不会不会
初始值可选必须

变量声明提升

变量提升指的是 变量在声明之前就可以被访问,但值为 undefined。这是因为 JS 在执行代码前会先解析并创建执行上下文,将变量和函数声明存入内存

变量提升的本质

  1. JS 代码执行前,先解析并创建执行上下文。
  2. 变量对象(VO) 存储了所有变量、函数声明、形参等信息。
  3. 作用域链的首端指向变量对象(VO),变量声明会被预加载,但不会赋值。

示例

javascript
console.log(a); // 输出:undefined
var a = 10;
console.log(a); // 输出:10

解析过程:

  1. 解析阶段var a 被存入变量对象,默认值为 undefined
  2. 执行阶段console.log(a) 访问 a,返回 undefined,然后 a = 10 赋值。

varletconst 的提升

  • var 声明的变量提升,但值为 undefined
  • letconst 存在暂时性死区(TDZ),不能在声明前访问
  • 函数声明整体提升,但 函数表达式不会提升
javascript
console.log(b); // 报错:Cannot access 'b' before initialization
let b = 20;
javascript
foo(); // 输出:"I am a function"

function foo() {
  console.log("I am a function");
}
javascript
bar(); // TypeError: bar is not a function
var bar = function () {
  console.log("I am a function expression");
};

总结

  1. 变量声明提升var 变量提升,初始值为 undefinedlet/const 变量不会被提升(存在暂时性死区)。
  2. 函数声明提升:整个函数体被提升,可以提前调用。
  3. 函数表达式不会提升var fn = function () {} 只提升 fn 变量,但不会赋值。

nullundefined

undefined(未定义)

  • 变量声明了但未赋值 时默认是 undefined
  • 访问 不存在的对象属性 也会返回 undefined
js
let a;
console.log(a); // undefined

const obj = {};
console.log(obj.name); // undefined

null(空对象)

  • null 表示有意为空,通常用于初始化可能存储对象的变量。
js
let user = null;
console.log(user); // null

对比

关键点undefinednull
含义变量未赋值有意为空
typeof"undefined""object"(历史遗留问题)
比较 ==truenull == undefined-
比较 ===false(类型不同)-

原型和原型链

原型(Prototype)

  • 每个对象 都有一个 __proto__ 指向其原型prototype)。
  • 共享属性和方法,实例对象可访问原型中的内容。
js
function Person(name) {
  this.name = name;
}
Person.prototype.sayHello = function () {
  console.log(`Hello, I'm ${this.name}`);
};

const john = new Person('John');
john.sayHello(); // "Hello, I'm John"

原型链(Prototype Chain)

属性查找机制:当访问对象属性时,JS 先在对象本身查找,找不到就沿着 __proto__ 向上查找,直到 null(即 Object.prototype 的终点)。

js
console.log(john.toString()); // Object.prototype.toString()

johnPerson.prototypeObject.prototypenull

对比

关键点__proto__prototype
作用对象的原型指向构造函数的原型对象
访问方式obj.__proto__Constructor.prototype
修改方式Object.setPrototypeOf(obj, proto)Constructor.prototype.method = function() {...}

原型继承

子类继承父类的原型,可扩展对象功能。

js
function Student(name, grade) {
  Person.call(this, name);
  this.grade = grade;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

const alice = new Student('Alice', 'A');
alice.sayHello(); // "Hello, I'm Alice"

记忆要点

  • 对象的 __proto__ 指向构造函数的 prototype
  • 查找属性时,JS 沿原型链向上查找,直到 null
  • 推荐使用 Object.getPrototypeOf() / Object.setPrototypeOf() 代替 __proto__

获取原型的方法

  1. Object.getPrototypeOf()(推荐)
    标准方法,返回对象的原型。

    js
    const obj = {};
    const protoObj = { key: 'value' };
    Object.setPrototypeOf(obj, protoObj);
    
    console.log(Object.getPrototypeOf(obj)); // { key: 'value' }
  2. __proto__(不推荐)
    直接访问或修改原型,已被 Object.getPrototypeOf() 取代。

    js
    const obj = {};
    const protoObj = { key: 'value' };
    obj.__proto__ = protoObj; // 不推荐
    
    console.log(obj.__proto__); // { key: 'value' }
  3. prototype(构造函数专属)
    仅用于构造函数,实例对象本身没有 prototype

    js
    function MyClass() {}
    const instance = new MyClass();
    
    console.log(instance.prototype); // undefined(实例没有 prototype)
    console.log(MyClass.prototype); // MyClass {}
  4. instanceof(检查原型链)
    判断对象是否继承自某个构造函数的原型

    js
    function Animal() {}
    const myAnimal = new Animal();
    
    console.log(myAnimal instanceof Animal); // true
    console.log(myAnimal instanceof Object); // true

总结

  • Object.getPrototypeOf(obj) 获取原型
  • Object.setPrototypeOf(obj, proto) 设置原型
  • __proto__ 仅作调试,不建议使用

继承的几种实现方式

原型链继承

通过将子类的原型设置为父类的实例,从而继承父类的属性和方法。

javascript
function Animal(name) {
  this.name = name;
}

Animal.prototype.sayName = function() {
  console.log('My name is ' + this.name);
};

function Dog(name, breed) {
  Animal.call(this, name); // 调用父类构造函数
  this.breed = breed;
}

Dog.prototype = new Animal();  // 子类原型指向父类实例
Dog.prototype.constructor = Dog;

var myDog = new Dog('Buddy', 'Labrador');
myDog.sayName(); // 输出:My name is Buddy

缺点:所有实例共享同一个原型对象,可能导致属性共享和覆盖的问题。

构造函数继承(借用构造函数)

通过在子类构造函数中调用父类构造函数,继承父类的属性。

javascript
function Animal(name) {
  this.name = name;
}

Animal.prototype.sayName = function() {
  console.log('My name is ' + this.name);
};

function Dog(name, breed) {
  Animal.call(this, name);  // 调用父类构造函数
  this.breed = breed;
}

var myDog = new Dog('Buddy', 'Labrador');
myDog.sayName(); // 输出:My name is Buddy

缺点:无法继承父类原型上的方法。

组合继承

结合了原型链继承和构造函数继承,通过调用父类构造函数设置实例属性,通过将子类原型设置为父类实例来继承父类的方法。

javascript
function Animal(name) {
  this.name = name;
}

Animal.prototype.sayName = function() {
  console.log('My name is ' + this.name);
};

function Dog(name, breed) {
  Animal.call(this, name);  // 调用父类构造函数
  this.breed = breed;
}

Dog.prototype = new Animal();  // 子类原型指向父类实例
Dog.prototype.constructor = Dog;

var myDog = new Dog('Buddy', 'Labrador');
myDog.sayName(); // 输出:My name is Buddy

缺点:调用了两次父类构造函数,既通过 Animal.call(this, name) 又通过 Dog.prototype = new Animal()

原型式继承

通过创建一个临时的构造函数,将一个对象作为该构造函数的原型,从而实现继承。

javascript
function createObject(obj) {
  function F() {}
  F.prototype = obj;
  return new F();
}

var parent = { name: 'Parent' };
var child = createObject(parent);
console.log(child.name); // 输出:Parent

缺点:存在属性共享的问题,所有继承的对象共享同一个原型。

寄生式继承

通过在一个函数内部创建一个对象,并对其进行扩展,然后返回该对象实现继承。

javascript
function createObject(obj) {
  var clone = Object.create(obj);  // 创建原型为 obj 的新对象
  clone.sayHello = function() {
    console.log('Hello!');
  };
  return clone;
}

var parent = { name: 'Parent' };
var child = createObject(parent);
console.log(child.name); // 输出:Parent
child.sayHello(); // 输出:Hello!

缺点:与原型链继承一样,存在属性共享的问题。

寄生组合式继承(推荐)

通过借用构造函数继承属性,通过原型链继承方法,解决了构造函数继承和原型链继承的缺点。

javascript
function Animal(name) {
  this.name = name;
}

Animal.prototype.sayName = function() {
  console.log('My name is ' + this.name);
};

function Dog(name, breed) {
  Animal.call(this, name);  // 借用构造函数
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);  // 原型链继承
Dog.prototype.constructor = Dog;

var myDog = new Dog('Buddy', 'Labrador');
myDog.sayName(); // 输出:My name is Buddy

优点

  • 时间复杂度 O(n)空间复杂度 O(1),不占用额外内存。
  • 既能继承父类的属性,又能继承父类的方法,是最常用的继承方式。

总结

继承方式优点缺点
原型链继承简单、直接属性共享问题,子类实例共享同一原型对象
构造函数继承继承父类实例属性无法继承父类原型方法
组合继承继承父类实例属性和原型方法调用父类构造函数两次
原型式继承简单、能继承属性属性共享问题
寄生式继承能扩展父类的功能属性共享问题
寄生组合式继承继承属性和方法,解决了上述问题实现稍复杂

推荐方式:寄生组合式继承,适用于大多数场景,既能继承属性又能继承方法,且避免了性能损失。

不同进制数字的表示方式

  • 0X0x 开头的表示为十六进制。
  • 00O0o 开头的表示为八进制。
  • 0B0b 开头的表示为二进制格式。

typeof NaN

  • NaN 意指“不是一个数字”(not a number),NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。

  • console.log(typeof NaN) //-> "number"

  • NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN != NaN 为 true。

parseInt()Number()

方法解析规则允许非数字字符失败返回
parseInt()从左到右解析,遇到非数字字符停止允许NaN
Number()只接受完整的数字字符串不允许NaN
  • parseInt() 解析带有前缀字符的数字
  • Number() 转换完整的数值字符串

字符串转换数字的方法

Number()(严格转换)

适用于完整数字字符串,不允许包含非数字字符,否则返回 NaN

js
console.log(Number("123"));    // 123 
console.log(Number("12.34"));  // 12.34 
console.log(Number("123abc")); // NaN(含非法字符)
console.log(Number(""));       // 0(空字符串视为 0)

parseInt()(提取整数,遇非数字停止)

适用于前面是数字的情况,可指定进制(默认 10)。

js
console.log(parseInt("123abc"));  // 123
console.log(parseInt("12.34"));   // 12(忽略小数部分)
console.log(parseInt("abc123"));  // NaN(非数字开头)
console.log(parseInt("0xF", 16)); // 15(16 进制解析)

parseFloat()(提取浮点数,遇非数字停止)

适用于带小数点的数值字符串

js
console.log(parseFloat("12.34abc")); // 12.34
console.log(parseFloat("123.45.67")); // 123.45(遇第二个 `.` 停止)
console.log(parseFloat("abc123")); // NaN

+(隐式转换,等价于 Number()

适用于直接转换数值字符串,简洁高效

js
console.log(+"123");   // 123
console.log(+"12.34"); // 12.34 
console.log(+"123abc"); // NaN
console.log(+"");      // 0(空字符串转换为 0)

作用域链

JavaScript 的作用域链(Scope Chain)是指在代码中查找变量和函数的过程中所形成的嵌套作用域的链式结构

  • 作用域链:变量查找的路径,从内到外,逐层向上查找
  • 形成机制:由函数和块级作用域的嵌套关系自动创建
  • 查找规则:先当前作用域 → 逐级向外 → 全局作用域 → 未找到则报错(ReferenceError
javascript
let global = "全局";
function test() {
  let local = "局部";
  console.log(local); // 先找当前作用域 ✓
  console.log(global); // 当前没有,往外层找 ✓
  console.log(unknown); // 全局也没有 → 抛出 ReferenceError
}

闭包(Closure)

闭包是指 一个函数能够访问其外部函数作用域中的变量,即使外部函数已经执行结束

闭包的形成

  1. 函数嵌套:闭包发生在函数内部定义了另一个函数的情况下。
  2. 内部函数引用外部变量:即使外部函数执行完毕,内部函数仍然“记住”并可以访问这些变量。
javascript
function outer() {
  var message = "Hello, Closure!";
  
  return function inner() {
    console.log(message);
  };
}

var closureFunc = outer();
closureFunc(); // 输出:"Hello, Closure!"

为什么能访问 message

  • inner 形成了闭包,保留了对 outer 作用域的访问权。
  • 即使 outer 执行结束,message 变量仍然存活,因为 inner 持有对它的引用。

闭包的应用

数据私有化(模拟私有变量)、回调函数、循环中的闭包问题等。

  • 数据私有化(模拟私有变量)

    javascript
    function counter() {
      let count = 0;
      
      return {
        increment: function() { count++; console.log(count); },
        decrement: function() { count--; console.log(count); }
      };
    }
    
    const myCounter = counter();
    myCounter.increment(); // 1
    myCounter.increment(); // 2
    myCounter.decrement(); // 1

    优点:外部无法直接修改 count,只能通过 incrementdecrement 访问,模拟了私有变量。

  • 回调函数

    javascript
    function delayedMessage(message, delay) {
      setTimeout(function() {
        console.log(message);
      }, delay);
    }
    
    delayedMessage("Hello after 2s", 2000);

    闭包确保 message 变量在 setTimeout 执行时仍然可用。

  • 循环中的闭包问题

    javascript
    for (var i = 0; i < 3; i++) {
      setTimeout(function() {
        console.log(i);
      }, 1000);
    }
    // 输出:3, 3, 3(不是 0, 1, 2)

    原因var i 是全局作用域变量,所有回调共享同一个 i,等 setTimeout 触发时,i 已经变成 3

    解决方案:用 let 或创建一个立即执行函数(IIFE)

    javascript
    for (let i = 0; i < 3; i++) {
      setTimeout(() => console.log(i), 1000);
    }
    // 输出:0, 1, 2

注意事项

闭包可能导致内存泄漏

  • 由于闭包引用外部变量,可能导致变量无法被垃圾回收,增加内存占用。
  • 解决方案:避免不必要的闭包,手动解除引用

this 关键字

this 指向当前执行上下文的对象,其值取决于函数的调用方式。

不同上下文中的 this

  1. 全局上下文

    • 在全局作用域中(即函数外),this 指向全局对象(浏览器中是 window,Node.js 中是 global)。
    • 示例:
      javascript
      console.log(this); // 浏览器中输出 Window 对象
  2. 普通函数调用

    • 在非严格模式下,直接调用函数时 this 指向全局对象;严格模式下为 undefined
    • 示例:
      javascript
      function show() {
        console.log(this);
      }
      show(); // 非严格模式下输出 Window;严格模式下输出 undefined
  3. 对象方法调用

    • 当函数作为对象的方法调用时,this 指向调用该方法的对象。
    • 示例:
      javascript
      const obj = {
        name: 'Alice',
        greet() {
          console.log(this.name);
        }
      };
      obj.greet(); // 输出 'Alice'
  4. 构造函数调用

    • 使用 new 关键字调用函数时,this 指向新创建的实例对象。
    • 示例:
      javascript
      function Person(name) {
        this.name = name;
      }
      const person = new Person('Bob');
      console.log(person.name); // 输出 'Bob'
  5. 显式绑定

    • 通过 callapplybind 方法可以手动指定 this 的值。
    • 示例:
      javascript
      function showName() {
        console.log(this.name);
      }
      const user = { name: 'Charlie' };
      showName.call(user); // 输出 'Charlie'
  6. 箭头函数

    • 箭头函数没有自己的 this,它会继承其定义时的外层作用域的 this
    • 示例:
      javascript
      const obj2 = {
        name: 'David',
        greet: () => {
          console.log(this.name);
        }
      };
      obj2.greet(); // 通常输出全局对象的 name(在浏览器中可能是 undefined)
  7. DOM 事件处理

    • 在事件处理函数中,this 指向触发事件的元素。
    • 示例:
      html
      <button id="btn">Click me</button>
      <script>
        document.getElementById('btn').addEventListener('click', function() {
          console.log(this); // 输出点击的按钮元素
        });
      </script>

总结

  • 调用方式决定 this:全局调用、对象方法、构造函数和显式绑定的调用方式分别决定了 this 的指向。
  • 严格模式与非严格模式:普通函数中严格模式下 thisundefined,非严格模式下为全局对象。
  • 箭头函数特点:箭头函数不绑定 this,继承外层作用域。

eval

  • 把对应的字符串解析成 JS 代码并运行。
  • 应该避免使用 eval,不安全、非常耗性能(2 次,一次解析成 js 语句,一次执行)。

DOM 和 BOM

DOM(文档对象模型)

  • 定义:DOM 将 HTML、XML 等文档解析为树状结构,每个节点代表文档中的一个部分(元素、文本、属性等)。

  • 用途:通过 JavaScript 操作页面内容与结构(增删改查),实现动态更新网页。

  • 常用 API

    • document.getElementById()document.querySelector():查找节点
    • element.innerHTMLelement.textContent:读写节点内容
    • appendChild()removeChild():节点的增删

BOM(浏览器对象模型)

  • 定义:BOM 表示浏览器窗口及其组件的对象模型,它提供与浏览器本身交互的 API,不属于 DOM 标准,具体实现可能存在差异。

  • 用途:主要用于操作浏览器窗口、地址栏、历史记录、弹窗等,如获取屏幕信息、重定向页面、控制浏览历史等。

  • 主要对象

    • window:全局对象,所有 BOM 对象均挂载在其上
    • navigator:浏览器信息(如浏览器名称、版本等)
    • screen:用户屏幕信息(如分辨率、颜色深度)
    • location:当前文档的 URL 信息(支持重定向、刷新)
    • history:浏览历史记录(支持前进、后退)
  • 常用操作

    javascript
    alert('Hello, World!');                        // 弹出警告框
    window.location.href = 'https://example.com';  // 页面重定向
    console.log(navigator.userAgent);              // 输出浏览器信息

总结

  • DOM 侧重于操作文档结构和内容,是页面内容的“内在模型”
  • BOM 则用于与浏览器窗口和环境交互,是页面外部环境的“接口”

事件

在 JavaScript 中,事件是指在程序执行过程中发生的一些事情,例如用户点击按钮、页面加载完成等。事件可以触发相应的事件处理函数,以执行特定的操作。

  1. 鼠标事件:

    • click: 当用户点击元素时触发。
    • mouseovermouseout: 当鼠标移入或移出元素时触发。
    • mousedownmouseup: 当鼠标按下或释放时触发。
  2. 键盘事件:

    • keydownkeyup: 当用户按下或释放键盘上的键时触发。
  3. 表单事件:

    • submit: 当用户提交表单时触发。
    • change: 当表单元素的值发生变化时触发。
    • input: 当输入框中的值发生变化时触发。
  4. 文档/窗口事件:

    • load: 当文档或页面完成加载时触发。
    • unload: 当用户离开页面时触发。
    • resize: 当窗口大小发生变化时触发。
  5. 焦点事件:

    • focusblur: 当元素获得或失去焦点时触发。
  6. 事件处理方式:

    • 内联事件处理: 直接在 HTML 标签中通过on属性定义,如<button onclick="myFunction()">Click me</button>
    • DOM 级别事件处理: 通过 JavaScript 代码将事件处理函数绑定到 DOM 元素,如element.addEventListener('click', myFunction)
  7. 事件对象:
    当事件发生时,会创建一个事件对象,其中包含有关事件的信息。事件对象作为参数传递给事件处理函数,可以使用该对象获取有关事件的详细信息,如触发事件的元素、鼠标位置等。

事件委托

事件委托(Event Delegation)是一种利用事件冒泡的特性,将事件处理程序绑定到一个父元素,从而减少事件处理程序数量的技术。通过这种方式,可以在单一的事件处理程序上管理多个子元素的事件,提高性能并简化代码。

工作原理

  1. 事件冒泡: 在 DOM 中,当一个事件(如点击)发生在某个元素上时,事件会从该元素开始向上冒泡至 DOM 树的根部。这是浏览器处理事件的默认行为。

  2. 事件委托: 将事件处理程序绑定到父元素,然后利用事件冒泡的过程来捕获子元素上的事件。

示例

考虑一个 ul 元素包含多个 li 元素的列表,我们想要为每个 li 元素添加点击事件处理程序。使用事件委托的方式如下:

html
<ul id="parentList">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
  <!-- more list items... -->
</ul>

<script>
  // 通过事件委托将点击事件处理程序绑定到父元素
  document.getElementById('parentList').addEventListener('click', function(event) {
    // 检查点击的是否是 li 元素
    if (event.target.tagName === 'LI') {
      console.log('Clicked on:', event.target.textContent);
    }
  });
</script>

在这个例子中,点击任何一个列表项都会触发父元素上的点击事件处理程序。通过检查 event.target,我们可以确定实际被点击的是哪个子元素。

优势

  1. 性能提升: 减少了事件处理程序的数量,特别是在大型列表或动态生成的内容中。
  2. 代码简化: 通过将事件处理逻辑集中在父元素上,代码更简洁易读。

"use strict"

"use strict" 是一种 ECMAscript5 添加的严格运行模式,使得 Javascript 可以在更严格的条件下运行。

作用:

  • 消除 Javascript 语法的一些不合理、不严谨之处,减少一些怪异行为。
  • 消除代码运行的一些不安全之处,保证代码运行的安全。
  • 提高编译器效率,增加运行速度。

区别:

  • 禁止使用 with 语句。
  • 禁止 this 关键字指向全局对象。
  • 对象不能有重名的属性。

instanceof 的作用

instanceof 运算符用于判断一个构造函数的 prototype 属性是否存在于某个对象的原型链中,从而确定该对象是否是该构造函数的实例。

原理

  1. 获取对象的原型(即通过 Object.getPrototypeOf() 获取)。
  2. 与构造函数的 prototype 属性进行比较。
  3. 如果相等则返回 true,否则继续向上查找,直至原型链末尾(null)。

实现示例

javascript
function myInstanceof(left, right) {
let proto = Object.getPrototypeOf(left);  // 获取对象的原型
let prototype = right.prototype;            // 获取构造函数的 prototype 对象

// 判断构造函数的 prototype 是否在对象的原型链上
while (true) {
  if (!proto) return false;
  if (proto === prototype) return true;
  proto = Object.getPrototypeOf(proto);
}
}

使用示例

javascript
function Person(name) {
  this.name = name;
}

const p = new Person('Alice');
console.log(myInstanceof(p, Person)); // 输出:true
console.log(myInstanceof(p, Array));  // 输出:false

new 操作符

new 操作符用于创建一个新的对象实例,它执行以下步骤:

  1. 创建一个新的空对象: 通过 new 创建一个新对象,它成为函数的实例。

    javascript
    var newObj = new SomeConstructor();
  2. 将构造函数的作用域赋给新对象: 新对象被赋予构造函数的 this 上下文。

    javascript
    function SomeConstructor() {
      this.property = 'value';
    }
    
    var newObj = new SomeConstructor();
    console.log(newObj.property); // 输出:value
  3. 执行构造函数的代码块: 新对象现在拥有了构造函数中定义的属性和方法。

    javascript
    function AnotherConstructor(name) {
      this.name = name;
      this.sayHello = function() {
        console.log('Hello, ' + this.name + '!');
      };
    }
    
    var anotherObj = new AnotherConstructor('John');
    anotherObj.sayHello(); // 输出:Hello, John!
  4. 返回新对象: 如果构造函数中没有明确返回一个对象,那么 new 操作符会返回新创建的对象实例。如果构造函数有返回值,且返回值是一个对象,那么返回该对象;如果是其他类型的值,依然返回新创建的对象。

    javascript
    function CustomConstructor() {
      this.value = 42;
      return { customValue: 'Custom' };
    }
    
    var customObj = new CustomConstructor();
    console.log(customObj.value); // 输出:undefined
    console.log(customObj.customValue); // 输出:Custom

浏览器缓存机制

浏览器缓存通过保存已获取资源,减少对服务器的请求,提高网页加载速度。主要包括 HTTP 缓存浏览器存储

HTTP 缓存

HTTP 缓存是通过 HTTP 头部来控制的,分为强缓存和协商缓存。

① 强缓存(不请求服务器)

  • Cache-Control: max-age=3600 → 资源在 3600 秒内有效,直接从缓存读取。
  • Expires: Tue, 20 Mar 2025 12:00:00 GMT → 过了指定时间后才请求服务器(已被 Cache-Control 取代)。

② 协商缓存(需向服务器验证)

  • Last-Modified + If-Modified-Since → 服务器返回资源最后修改时间,浏览器下次请求时对比。
  • ETag + If-None-Match → 服务器生成唯一标识符,浏览器对比是否变更。
  • 若资源未变更,服务器返回 304 Not Modified,浏览器使用缓存。

浏览器存储

  • Cookie → 存储少量数据,常用于用户身份识别(受 HttpOnly 限制)。
  • LocalStorage → 长期存储(关闭浏览器不会消失)。
  • SessionStorage → 仅当前会话(关闭页面即清除)。

缓存优先级

  1. 强缓存Cache-Control / Expires)→ 直接使用缓存,不发请求。
  2. 协商缓存ETag / Last-Modified)→ 向服务器验证,未变更则返回 304
  3. 本地存储LocalStorage / SessionStorage)→ 仅适用于前端手动存储的数据。

Ajax 解决浏览器缓存问题

Ajax 请求时,浏览器可能缓存响应数据,导致后续请求获取的是旧数据。

常用解决方案:

  1. 添加时间戳/随机数

    • 在 URL 后附加一个唯一参数(如时间戳或随机数),确保每次请求 URL 不同,从而避免缓存。
    • 示例:
      javascript
      // 使用时间戳
      var timestamp = new Date().getTime();
      var url = 'example.com/api/data?timestamp=' + timestamp;
      // 或者使用随机数
      var randomNumber = Math.random();
      var url = 'example.com/api/data?random=' + randomNumber;
  2. 设置 HTTP 响应头

    • 在服务器端配置响应头,禁用缓存。常用的响应头有:
      • Cache-Control: no-cache, no-store, must-revalidate
      • Pragma: no-cache
      • Expires: 0
    • 示例(Express 框架):
      javascript
      app.get('/api/data', function(req, res) {
        res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
        res.setHeader('Pragma', 'no-cache');
        res.setHeader('Expires', '0');
        // 返回响应数据
      });
  3. 使用 POST 请求 与 GET 请求相比,POST 请求通常不会被浏览器缓存,因此可以用来获取最新数据。

浏览器的同源策略

同源策略(Same-Origin Policy) 是浏览器的重要安全机制,用于限制不同来源的网页之间的交互,以防止恶意网站窃取数据或执行未授权的操作。

什么是“同源”

同源 指的是 协议(protocol)、域名(host)和端口(port) 必须一致。

同源策略的限制

同源策略会阻止以下行为:

  1. 跨域读取 Cookie、LocalStorage 和 IndexedDB
  2. 跨域访问 DOM
  3. 跨域发送 Ajax 请求(fetch、XMLHttpRequest)

如何解决跨域问题

浏览器提供了一些方式允许跨域访问:

  1. CORS(跨域资源共享)

    • 服务器在响应头中添加 Access-Control-Allow-Origin,允许特定域访问:
      http
      Access-Control-Allow-Origin: https://allowed-origin.com
    • 服务器还可以允许多个域,或直接设置 * 允许所有来源(不推荐):
      http
      Access-Control-Allow-Origin: *
  2. JSONP(仅适用于 GET 请求)

    • 通过动态创建 <script> 标签实现加载跨域数据:
      html
      <script src="https://api.example.com/data?callback=myFunction"></script>
    • 服务器返回的数据会调用 myFunction 处理:
      javascript
      function myFunction(data) {
        console.log(data);
      }
  3. 代理服务器

    • 通过后端代理请求目标服务器,避免直接跨域:
      javascript
      fetch('/proxy/api/data')  // 服务器会代理转发请求
        .then(response => response.json())
        .then(data => console.log(data));
  4. WebSocket:WebSocket 不受同源策略的限制,可以在任意域名上建立全双工的通信。

  5. 使用跨域资源的 HTML 标签:例如 <img><link><script> 等标签不受同源策略的限制,可以加载跨域资源。

模块化规范

JavaScript 支持多种模块化规范,其中一些是语言本身提供的,而另一些则是由社区和第三方库引入的。以下是常见的 JavaScript 模块化规范:

CommonJS

  • 特点:同步加载
  • 适用场景:Node.js 服务器端和部分前端场景
  • 导出/导入module.exports / require
  • 示例
    javascript
    // module.js
    module.exports = { greeting: 'Hello, CommonJS!' };
    
    // main.js
    const myModule = require('./module');
    console.log(myModule.greeting);

CMD (Common Module Definition)

  • 特点:异步加载模块,按需加载(SeaJS 实现)
  • 适用场景:浏览器端按需加载
  • 导出/导入:通过 define 定义,通过 require 异步加载
  • 示例
    javascript
    // module.js
    define(function(require, exports, module) {
      exports.greeting = 'Hello, CMD!';
    });
    
    // main.js
    define(function(require, exports, module) {
      const myModule = require('./module');
      console.log(myModule.greeting);
    });

AMD (Asynchronous Module Definition)

  • 特点:专为浏览器异步加载设计(RequireJS 实现)
  • 适用场景:浏览器异步加载模块
  • 导出/导入:使用 define 定义模块,require 加载
  • 示例
    javascript
    // module.js
    define([], function() {
      return { greeting: 'Hello, AMD!' };
    });
    
    // main.js
    require(['module'], function(myModule) {
      console.log(myModule.greeting);
    });

UMD (Universal Module Definition)

  • 特点:兼容 CommonJS、AMD 和全局变量
  • 适用场景:既适用于 Node.js,又适用于浏览器和其他环境
  • 示例
    javascript
    (function (root, factory) {
      if (typeof define === 'function' && define.amd) {
        define(['exports'], factory);
      } else if (typeof exports === 'object') {
        factory(exports);
      } else {
        root.myModule = {};
        factory(root.myModule);
      }
    }(this, function (exports) {
      exports.greeting = 'Hello, UMD!';
    }));

ES6 模块

  • 特点:语言原生支持,静态分析
  • 适用场景:现代 JavaScript 开发
  • 导出/导入export / import
  • 示例
    javascript
    // module.js
    export const greeting = 'Hello, ES6 Module!';
    
    // main.js
    import { greeting } from './module';
    console.log(greeting);

SystemJS

  • 特点:动态加载模块,支持多种模块格式(AMD、CommonJS、ES6)
  • 适用场景:浏览器和服务器端的动态模块加载
  • 示例
    javascript
    // module.js
    System.register([], function(exports) {
      exports('greeting', 'Hello, SystemJS!');
    });
    
    // main.js
    System.import('./module').then(module => {
      console.log(module.greeting);
    });

AMD 和 CMD 规范的区别

对比维度AMDCMD
依赖声明依赖前置:模块定义时必须显式声明所有依赖就近依赖:依赖可在需要时通过 require 加载
执行时机依赖加载完毕后即执行,执行顺序可能与代码书写顺序不一致依赖仅下载,不立即执行;等到遇到 require 时才执行,保持书写顺序

ES6 和 CommonJS 规范的区别

对比维度CommonJS 模块ES6 模块
输出机制输出值的拷贝:模块内部变化不会影响已导出值输出值的引用:模块内部变化会实时反映到引用上
加载时机运行时加载:模块整个加载后才创建对象,再从中读取编译时输出接口:静态解析阶段生成只读引用

requireJS 的核心原理

require.js 的核心原理是通过动态创建 script 脚本来异步引入模块,然后对每个脚本的 load 事件进行监听,如果每个脚本都加载完成了,再调用回调函数。

.call().apply()

.call().apply() 都是 JavaScript 中用于调用函数的方法,它们的作用是改变函数的执行上下文(this 指向)并立即执行该函数

区别与注意事项

  1. 参数传递方式.call()逐个传递参数,而 .apply() 接受一个数组作为参数。

  2. 性能:由于 .call() 在语法上更简洁,通常比 .apply() 略微更快。但差异通常很小,优化程度可能因引擎而异。

  3. 使用场景:在需要动态传递参数且参数数量不确定的情况下,.apply() 更方便。若参数数量已知,.call() 更简洁。

V8 引擎的垃圾回收机制

V8 是由 Google 开发的高性能 JavaScript 引擎,主要用于 Chrome 浏览器和 Node.js 等 JavaScript 运行环境。V8 引擎的垃圾回收机制是其性能优势的一个重要组成部分。

以下是关于 V8 引擎的垃圾回收机制的主要特点:

分代垃圾回收

V8 引擎采用分代垃圾回收策略,将内存分为新生代老生代两个代。新生代中的对象生命周期较短,老生代中的对象生命周期较长。

  • 新生代: 使用副垃圾回收器,采用复制算法。将新生代内存分为两个区域:From 空间和 To 空间。对象首先被分配到 From 空间,当 From 空间满时,会启动垃圾回收,将存活的对象复制到 To 空间,然后清空 From 空间。在复制的同时,对象的存活时间增长,最终会晋升到老生代。

  • 老生代: 使用主垃圾回收器。主要分为标记阶段、清除阶段和整理阶段。标记阶段用于标记存活的对象,清除阶段用于清除未标记的对象,整理阶段用于压缩内存空间,减少内存碎片。

增量标记

V8 引擎采用增量标记(Incremental Marking)的方式执行垃圾回收,将整个垃圾回收过程分解为多个小步骤,穿插在 JavaScript 执行中。这样可以减小单次垃圾回收的时间,降低对用户代码的影响。

空闲时执行垃圾回收

V8 引擎利用 JavaScript 执行空闲时间来执行垃圾回收。当浏览器空闲时,例如用户停止滚动或打开其他标签页,V8 将尽可能地执行垃圾回收操作,以最小化对用户交互的干扰。

快速分配

为了提高对象分配的效率,V8 引擎采用了快速分配的策略。通过将新对象分配在新生代的 To 空间,而不是等待垃圾回收的完成,从而避免了等待新生代的垃圾回收操作。

内存限制

为了防止内存过度膨胀,V8 引擎会设置内存限制。当内存占用接近限制时,V8 会主动触发垃圾回收,尽可能释放不再使用的内存。

Polyfill

Polyfill 是指用于在旧版本浏览器中实现新的 Web 标准功能的代码。由于不同浏览器在实现 Web 标准方面存在差异,一些新特性可能不受一些旧版本浏览器的支持。为了弥补这些缺失,开发者可以使用 Polyfill 来模拟这些新特性,以便在旧版本浏览器中实现相同的功能。

Polyfill 的目标是在尽可能少的代码和修改的情况下,提供对新特性的支持。它可以是一个 JavaScript 库、脚本或代码片段,通常由开发者或社区提供。

节流与防抖

节流

定义:规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效

使用场景:窗口 resize、scroll、输入框 input、频繁点击等

js
function throttle(fn, delay) {
  var preTime = Date.now();

  return function () {
    var context = this,
      args = arguments,
      nowTime = Date.now();

    // 如果两次时间间隔超过了指定时间,则执行函数。
    if (nowTime - preTime >= delay) {
      preTime = Date.now();
      return fn.apply(context, args);
    }
  };
}

防抖

定义:在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时

使用场景:搜索框输入搜索、点击提交等

js
function debounce(fn, wait) {
  var timer = null;

  return function () {
    var context = this,
      args = arguments;

    // 如果此时存在定时器的话,则取消之前的定时器重新记时
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    // 设置定时器,使事件间隔指定事件后执行
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  };
}

事件循环

JavaScript 的事件循环(Event Loop)是一种异步编程模型,用于处理事件和回调函数,确保在单线程执行环境中能够处理异步任务,保持响应性并避免阻塞主线程。

事件循环的核心组成

  1. 调用栈(Call Stack)执行同步任务,函数调用进栈,执行完出栈。

  2. 任务队列(Task Queues)存放异步任务的回调

    • 宏任务(Macro Task):包含 setTimeoutsetInterval、DOM 事件、I/O 操作、UI 渲染等。
    • 微任务(Micro Task):包含 Promise.thenMutationObserverqueueMicrotask优先级更高

事件循环的执行流程

  1. 同步代码直接执行(比如 console.log)。
  2. 遇到异步任务(比如 setTimeoutPromise),丢给浏览器后台处理,不阻塞主线程
  3. 主线程空闲时(同步代码执行完),开始检查两个队列:
    • 微任务队列Promise.thenasync/awaitMutationObserver
    • 宏任务队列setTimeoutsetInterval、DOM 事件、AJAX 回调。
  4. 清空微任务队列(执行所有微任务,直到队列为空。微任务优先于宏任务,且新微任务会在此阶段被立即执行。)
  5. 重复循环 (取下一个宏任务,开始新的事件循环。)

执行顺序示例

javascript
console.log('1'); // 同步任务,直接输出
setTimeout(() => console.log('2'), 0); // 宏任务,加入队列
Promise.resolve().then(() => console.log('3')); // 微任务,加入队列
console.log('4'); // 同步任务,直接输出

输出顺序1 → 4 → 3 → 2

  • 同步代码先执行(14)。
  • 微任务队列优先于宏任务,32 前输出。

关键概念

  • 宏任务setTimeoutsetInterval、I/O、UI 渲染、事件回调。
  • 微任务Promise.thenMutationObserverprocess.nextTick(Node.js)。
  • 优先级:微任务 > 宏任务。每次事件循环处理完一个宏任务后,会清空所有微任务。

深浅拷贝

浅拷贝(Shallow Copy)

只复制第一层属性,如果属性是引用类型(如对象、数组),复制的是引用地址,新旧对象共享同一块内存。
常见方法

  • Object.assign({}, obj)
  • { ...obj }(展开运算符)
javascript
let obj1 = { name: "Tom", info: { age: 25 } };
let obj2 = { ...obj1 };
obj2.info.age = 30; 

console.log(obj1.info.age); // 30(obj1 也被修改)

深拷贝(Deep Copy)

递归复制所有层级,创建全新的对象,不会共享引用,修改不会影响原对象。
常见方法

  • JSON.parse(JSON.stringify(obj)(但会丢失 undefinedSymbolFunction
  • 手写递归拷贝
  • structuredClone(obj)(现代浏览器推荐,支持 DateRegExp
javascript
let obj1 = { name: "Tom", info: { age: 25 } };
let obj2 = JSON.parse(JSON.stringify(obj1));
obj2.info.age = 30;

console.log(obj1.info.age); // 25(obj1 不受影响)

深浅拷贝对比

拷贝方式是否共享引用是否复制所有层级适用场景
浅拷贝仅第一层结构简单
深拷贝递归所有层级复杂对象

函数柯里化(Currying)

柯里化是什么

把一个多参数函数,拆成一系列单参数函数,每次只传一个参数,直到所有参数传递完毕,才返回最终结果。
柯里化 = 拆分参数,多步调用

示例对比

javascript
// 普通函数
function add(a, b, c) {
  return a + b + c;
}
console.log(add(1, 2, 3)); // 6

// 柯里化函数
function curryAdd(a) {
  return function (b) {
    return function (c) {
      return a + b + c;
    };
  };
}
console.log(curryAdd(1)(2)(3)); // 6

区别

  • 普通调用:一次性传入所有参数 add(1, 2, 3)
  • 柯里化调用分步传参 curryAdd(1)(2)(3)

柯里化的优势

参数复用:固定一部分参数,生成新函数
延迟执行:部分参数先传,剩下的参数以后再传
函数组合:更容易创建高阶函数,提升代码复用性

javascript
const multiply = (a) => (b) => a * b;
const double = multiply(2);
console.log(double(5)); // 10(相当于 multiply(2, 5))

适用于高阶函数、事件处理、数据处理等场景。

异步编程

JavaScript 中实现异步编程的主要方式:回调函数、Promise 对象、Async/Await 和 Generator 函数等。

回调函数(Callback)

回调函数是最基本的异步编程方式,通过将函数作为参数传递给其他函数,在异步操作完成时执行回调函数。

javascript
function fetchData(callback) {
  // 模拟异步操作
  setTimeout(function() {
    const data = 'Async data';
    callback(data);
  }, 1000);
}

function processData(data) {
  console.log('Processed data:', data);
}

fetchData(processData);

Promise 对象

Promise 是一种更强大、更灵活的异步编程方式,用于处理异步操作的结果。它包含三个状态:Pending(进行中)、Resolved(已完成)、Rejected(已拒绝)。

javascript
function fetchData() {
  return new Promise((resolve, reject) => {
    // 模拟异步操作
    setTimeout(() => {
      const success = true;
      if (success) {
        const data = 'Async data';
        resolve(data);
      } else {
        reject('Error occurred');
      }
    }, 1000);
  });
}

fetchData()
  .then((data) => {
    console.log('Resolved:', data);
  })
  .catch((error) => {
    console.error('Rejected:', error);
  });

Async/Await

Async/Await 是基于 Promise 的语法糖,使得异步代码更加清晰和易读。async 关键字用于定义异步函数,await 关键字用于等待 Promise 对象的解决。

javascript
async function fetchData() {
  return new Promise((resolve, reject) => {
    // 模拟异步操作
    setTimeout(() => {
      const success = true;
      if (success) {
        const data = 'Async data';
        resolve(data);
      } else {
        reject('Error occurred');
      }
    }, 1000);
  });
}

async function processData() {
  try {
    const data = await fetchData();
    console.log('Resolved:', data);
  } catch (error) {
    console.error('Rejected:', error);
  }
}

processData();

Generator 函数

Generator 函数是一种特殊类型的函数,通过使用 yield 关键字可以将函数的执行暂停,并通过迭代器控制函数的执行。

javascript
function* fetchData() {
  // 模拟异步操作
  setTimeout(function() {
    const data = 'Async data';
    iterator.next(data);
  }, 1000);
  const data = yield;
}

const iterator = fetchData();
iterator.next(); // 启动 Generator 函数

function processData(data) {
  console.log('Processed data:', data);
}

fetchData()
  .then(processData)
  .catch((error) => {
    console.error('Rejected:', error);
  });

Proxy

Proxy 是 ECMAScript 6(ES6)引入的一种元编程特性,它允许你创建一个代理对象,用于拦截对目标对象的各种操作。通过使用 Proxy,你可以自定义目标对象的行为,实现自定义的操作,比如拦截属性的读取、写入、删除等。

Proxy 的基本语法如下:

javascript
const proxy = new Proxy(target, handler);
  • target:要代理的目标对象。
  • handler:一个包含各种代理操作的处理器对象。

下面是一些 Proxy 的常用拦截操作:

get 拦截器

用于拦截对象属性的读取操作。

javascript
const target = {
  name: 'John',
  age: 30,
};

const handler = {
  get: function (target, prop) {
    console.log(`Reading property: ${prop}`);
    return target[prop];
  },
};

const proxy = new Proxy(target, handler);

console.log(proxy.name); // 会触发 get 拦截器,输出:Reading property: name

set 拦截器

用于拦截对象属性的设置操作。

javascript
const target = {
  name: 'John',
  age: 30,
};

const handler = {
  set: function (target, prop, value) {
    console.log(`Setting property: ${prop} to ${value}`);
    target[prop] = value;
  },
};

const proxy = new Proxy(target, handler);

proxy.age = 31; // 会触发 set 拦截器,输出:Setting property: age to 31

deleteProperty 拦截器

用于拦截对象属性的删除操作。

javascript
const target = {
  name: 'John',
  age: 30,
};

const handler = {
  deleteProperty: function (target, prop) {
    console.log(`Deleting property: ${prop}`);
    delete target[prop];
  },
};

const proxy = new Proxy(target, handler);

delete proxy.age; // 会触发 deleteProperty 拦截器,输出:Deleting property: age

apply 拦截器

用于拦截函数的调用操作。

javascript
const target = function (a, b) {
  return a + b;
};

const handler = {
  apply: function (target, thisArg, argumentsList) {
    console.log(`Calling function with arguments: ${argumentsList.join(', ')}`);
    return target.apply(thisArg, argumentsList);
  },
};

const proxy = new Proxy(target, handler);

console.log(proxy(2, 3)); // 会触发 apply 拦截器,输出:Calling function with arguments: 2, 3

Proxy 的拦截器还有其他很多,可以根据需要进行使用,使得你能够更灵活地控制目标对象的行为。使用 Proxy 可以实现一些高级的元编程功能,例如数据绑定、拦截器合成等。

Promise 对象 和 Promises/A+ 规范

  • Promise 对象是 JavaScript 中异步编程的一种解决方案,最早由社区提出,在 ECMAScript 2015(ES6)规范中被引入,用于更清晰、更可靠地处理异步代码,避免了回调地狱(callback hell)的问题。

  • Promises/A+ 规范是 JavaScript Promise 的标准,规定了一个 Promise 所必须具有的特性。

  • Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。

    javascript
    // 创建 Promise
    const myPromise = new Promise((resolve, reject) => {
      // 异步操作,可以是网络请求、文件读取等
      // 如果成功,调用 resolve 并传递结果
      // 如果失败,调用 reject 并传递错误原因
    });
  • 一个 Promise 实例有三种状态

    • Pending(进行中): 初始状态,表示异步操作尚未完成。
    • Resolved(已完成): 表示异步操作成功完成。
    • Rejected(已拒绝): 表示异步操作失败。
  • Promise 方法

    • then() 和 catch()

      javascript
      myPromise
        .then((result) => {
          console.log('Resolved:', result);
        })
        .catch((error) => {
          console.error('Rejected:', error);
        });
  • Promise.all()

    用于同时处理多个 Promise 对象,当所有 Promise 对象都变为 Resolved 状态时,返回一个包含所有结果的数组;如果任一 Promise 对象变为 Rejected 状态,则直接进入 catch 分支。

    javascript
    const promise1 = Promise.resolve('Promise 1');
    const promise2 = Promise.resolve('Promise 2');
    const promise3 = new Promise((resolve, reject) => {
      setTimeout(() => resolve('Promise 3'), 1000);
    });
    
    Promise.all([promise1, promise2, promise3])
      .then((results) => {
        console.log('All promises fulfilled:', results);
      })
      .catch((error) => {
        console.error('One of the promises is rejected:', error);
      });
  • Promise.race()

    用于处理多个 Promise 对象,返回最先完成的 Promise 对象的结果或错误

    javascript
    const promise1 = new Promise((resolve, reject) => {
      setTimeout(() => resolve('Promise 1'), 2000);
    });
    const promise2 = Promise.resolve('Promise 2');
    
    Promise.race([promise1, promise2])
      .then((result) => {
        console.log('The first promise fulfilled:', result);
      })
      .catch((error) => {
        console.error('The first promise is rejected:', error);
      });

开发中常用的几种 Content-Type

  • application/x-www-form-urlencoded
    浏览器的原生 form 表单,如果不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded 方式提交数据。该种方式提交的数据放在 body 里面,数据按照 key1=val1&key2=val2 的方式进行编码,key 和 val 都进行了 URL 转码。

  • multipart/form-data
    该种方式也是一个常见的 POST 提交方式,通常表单上传文件时使用该种方式。

  • application/json
    告诉服务器消息主体是序列化后的 JSON 字符串。

  • text/xml
    该种方式主要用来提交 XML 格式的数据。

判断一个对象是否为空对象

js
function checkNullObj(obj) {
  return Object.keys(obj).length === 0;
}

数组随机排序的方法

sort() + Math.random()(不推荐)

js
arr.sort(() => Math.random() - 0.5);

特点:

  • 代码简洁,适用于小数组。
  • 不完全随机,sort() 的比较函数要求是传递性的,但 Math.random() 结果不满足这个条件,可能导致某些排列出现的概率不均衡。

新建数组(逐个随机抽取元素)

js
function randomSort(arr) {
  let result = [];
  let copy = [...arr]; // 复制原数组,防止修改原数组

  while (copy.length > 0) {
    let randomIndex = Math.floor(Math.random() * copy.length);
    result.push(copy[randomIndex]);
    copy.splice(randomIndex, 1);
  }

  return result;
}

特点:

  • 适用于不可变数据(不会修改原数组)。
  • 需要额外的存储空间,空间复杂度 O(n)
  • splice() 操作效率不高,导致时间复杂度较高。

洗牌算法(Fisher-Yates Shuffle,推荐)

js
function shuffle(arr) {
  let len = arr.length;

  for (let i = len - 1; i > 0; i--) {
    let randomIndex = Math.floor(Math.random() * (i + 1)); // 0 ~ i 之间的随机索引
    [arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]]; // 交换
  }

  return arr;
}

特点:

  • 随机性强,能实现均匀分布。
  • 时间复杂度 O(n)空间复杂度 O(1),不占额外空间。
  • 修改了原数组(如果不希望修改,可以 arr.slice() 复制一份)。

ES6 简写版(基于洗牌算法)

js
const shuffle = (arr) => arr.map((_, i, a) => [Math.random(), a[i]])
  .sort(([a], [b]) => a - b)
  .map(([_, v]) => v);

特点:

  • 代码简洁,基于 sort() 实现真正的随机排序。
  • 时间复杂度较 Fisher-Yates 稍高(O(n log n)),但适用于一些轻量级场景。

总结

方法适用场景时间复杂度空间复杂度随机性是否修改原数组
sort(() => Math.random() - 0.5)简单应用,小数组O(n log n)O(1)不完全随机
随机抽取新数组适用于不可变数据O(n²)(splice 影响)O(n)完全随机
洗牌算法(Fisher-Yates)推荐,效率最高O(n)O(1)完全随机
ES6 简写可选O(n log n)O(n)完全随机

推荐使用:Fisher-Yates 洗牌算法,最均匀、最快速、最省内存。

JavaScript 中倒计时的纠偏实现

一般通过 setTimeoutsetInterval 方法来实现一个倒计时效果。但是使用这些方法会存在时间偏差的问题,这是由于 JavaScript 的程序执行机制造成的,setTimeoutsetInterval 的作用是隔一段时间将回调事件加入到事件队列中,事件并不是立即执行的,它会等到当前执行栈为空的时候再取出事件执行,因此事件等待执行的时间就是造成误差的原因。

一般解决倒计时中的误差的有这样两种办法:

  • 第一种是通过前端定时向服务器发送请求获取最新的时间差,以此来校准倒计时时间。

  • 第二种方法是前端根据偏差时间来自动调整间隔时间的方式来实现的。这一种方式首先是以 setTimeout 递归的方式来实现倒计时,然后通过一个变量来记录已经倒计时的秒数。每一次函数调用的时候,首先将变量加一,然后根据这个变量和每次的间隔时间,我们就可以计算出此时无偏差时应该显示的时间。然后将当前的真实时间与这个时间相减,这样我们就可以得到时间的偏差大小,因此我们在设置下一个定时器的间隔大小的时候,我们就从间隔时间中减去这个偏差大小,以此来实现由于程序执行所造成的时间误差的纠正。

Released under the AGPL-3.0 License