JavaScript - 常见问题

127 阅读21分钟

1. JavaScript 数据类型

  • 基本类型String, Number, Boolean, Undefined, Null, Symbol。这些类型的数据是不可变的,存储在栈中。
    • NaN:是Number(数值)类型,但它代表的是一个“不是数字”(Not-A-Number)值,不会抛出错误。
  • 引用类型ObjectArray, Funtion。这些类型的数据可以修改它们的内容,且是通过引用传递的,存储在堆中。

2. 判断数据的类型

  • typeof 运算符

    • 返回数据类型,是字符串。
    typeof '123' // "string"
    typeof 123 // "number"
    typeof true // "boolean"
    typeof undefined // "undefined"
    typeof null // "object"(特殊情况)
    typeof Symbol() // "symbol"
    
    
  • instanceof 运算符

    • 判断对象是否是某个构造函数的实例,是布尔值。(不能判断原始类型)
    const arr = [1, 2, 3];
    console.log(arr instanceof Array); // true
    console.log(arr instanceof Object); // true,Array 本身是 Object 的子类
    
    function Person(name) {
       this.name = name;
     }
    const p = new Person('Alice');
    console.log(Person instanceof Function); // true
    console.log(Person instanceof Object); // true
    console.log(p instanceof Person); // true
    console.log(p instanceof Object); // true
    
  • Object.prototype.toString.call() 方法

    • 精确判断所有类型(推荐)。
    Object.prototype.toString.call([]); // "[object Array]"
    Object.prototype.toString.call(null); // "[object Null]"
    
  • constructor 属性

    • 判断对象由哪个构造函数创建。
    const arr = [1, 2, 3];
    console.log(arr.constructor === Array); // true
    

3. null 和 undefined 的区别

  • null:是一个指示“无”或“空”的特殊值。null 被认为是一个对象类型。转换为数字时为 0
  • undefined:是表示“变量未定义”或“未初始化”的特殊值,表示变量声明后尚未赋值。undefined 转换为数字时为 NaN

4. "==" 与 "===" 的区别

  • ==:执行类型转换后再比较值。
    • 例如:'1' == 1 会返回 true,因为类型会被自动转换成相同的类型。
  • ===:不进行类型转换,只有类型和值都相等时才返回 true
    • 例如:'1' === 1 会返回 false,因为类型不同。
 '' == '0' // false
 0 == '' // true
 0 == '0' // true
 false == 'false' // false
 false == '0' // true
 false == undefined // false
 false == null // false
 null == undefined // true
 ' \t\r\n' == 0 // true

5. 字符串常用方法

    • 使用 +${} 拼接字符串。
    • concat() 将多个字符串合并。
let stringValue = "hello ";
let result = stringValue.concat("world");
console.log(result); // "hello world"
console.log(stringValue); // "hello"
    • slice(), substr(), 和 substring() 提取字符串的子串。
let stringValue = "hello world";
console.log(stringValue.slice(3)); // "lo world"
console.log(stringValue.substr(3)); // "lo world"
console.log(stringValue.substring(3)); // "lo world"
console.log(stringValue.slice(3, 7)); // "lo w"
console.log(stringValue.substr(3, 7)); // "lo worl"
console.log(stringValue.substring(3, 7)); // "lo w"
    • trim(), trimLeft(), trimRight():去除字符串两端的空白。
    • repeat():重复字符串指定次数。
    • padStart(), padEnd():在字符串的两端填充指定字符,直到字符串达到指定长度。
    • toUpperCase()toLowerCase():大小写转化。
    • charAt():返回指定索引位置的字符。
    • indexOf() :返回字符所在的索引位置。
    • includes()startsWith(), endsWith():检查字符串是否包含,或以某个字符串开头或结尾。

6. 数组常用方法

    • push():向数组末尾添加元素,返回新数组的长度。
    • unshift():向数组开头添加元素,返回新数组长度。
    • concat():连接多个数组,返回一个新的数组,不修改原数组。
    • pop():移除数组末尾的元素,返回该元素。
    • shift():移除数组开头的元素,返回该元素。
    • slice():返回数组的一个子数组,不修改原数组。
    • splice():用于添加、删除或替换数组元素,返回操作元素。
    • indexOf():返回元素首次出现的索引,找不到返回 -1。
    • includes():判断数组中是否存在某个元素。
    • find():返回第一个符合条件的元素。
  • 排序

    • sort():根据指定的排序规则对数组进行排序,默认按字符编码排序,可以传入比较函数来进行自定义排序。
    • reverse():反转数组的顺序。
  • 迭代

    • forEach():遍历数组,适合执行副作用操作。
    • map():对数组的每个元素进行转换,返回新数组。
    • filter():过滤符合条件的数组项,返回新数组。
    • reduce():将数组中的所有元素按照某种规则累加/组合,返回单个值。
  • 转换

    • join():将数组转换为一个字符串,使用指定的分隔符连接各个元素。
    • toString():将数组转换为一个字符串,元素之间以逗号分隔。
    • from():将类数组对象或可迭代对象转换为数组。

7. 数组的特殊操作(扁平化、去重、乱序)

扁平化:将多维数组“拉平”为一维或指定深度的数组。

  • flat()
const arr = [1, [2, [3, [4]]]];
console.log(arr.flat(2)); // [1, 2, 3, [4]]
console.log(arr.flat(Infinity)); // [1, 2, 3, 4]

flat(depth) 参数控制展开深度,参数为 Infinity 可完全扁平化。

  • 递归
function flatten(arr) {
  return arr.reduce((acc, val) =>
    Array.isArray(val) ? acc.concat(flatten(val)) : acc.concat(val), []);
}

数组去重

  • Set[...]
const arr = [1, 2, 2, 3];
const unique = [...new Set(arr)];
console.log(unique); // [1, 2, 3]
  • filterindexOf
const unique = arr.filter((item, index) => arr.indexOf(item) === index);

数组乱序

  • sort() + Math.random()
const arr = [1, 2, 3, 4, 5];
const shuffled = arr.sort(() => Math.random() - 0.5);

8. 原型与原型链

  • 原型(Prototype):每个对象都有一个内部属性 [[Prototype]](可通过 __proto__ 访问),指向另一个对象。对象可以通过原型继承属性和方法。

  • 原型链(Prototype Chain):对象的原型也有自己的原型,层层向上形成链式结构。

    • 当访问属性或方法时,会沿原型链逐级查找,直到找到或到达链顶(null)。

9. 作用域与作用域链

  • 作用域(Prototype):变量可以被访问的范围。

    • 全局作用域:在代码任何地方都能访问到的变量,比如最外层声明的变量。
    • 函数作用域:在函数内部声明的变量,只能在函数内部访问。
    • 块级作用域(ES6引入):由一对 {} 包裹起来的范围(比如 iffor 里面),用 letconst 声明的变量才有块级作用域。
  • 作用域链(Scope Chain):查找变量时从当前作用域层层向外查找的一种规则。

    • 当查找变量时,会沿作用域链逐级向外查找,直到找到或到达全局作用域报错(ReferenceError)。

10. 闭包

  • 闭包:函数内部可以访问外部函数作用域中的变量。

    function outer() {
      let count = 0; // 外部函数作用域的变量
    
      return function inner() {
        count++;
        console.log(count);
      }
    }
    
    const counter = outer();  // outer 执行并返回 inner 函数
    counter();  // 输出: 1
    counter();  // 输出: 2
    
    • outer() 执行时,它返回了 inner() 函数。
    • counter 引用了 inner(),即使 outer() 已经执行完毕,inner() 仍然保留着对 count 的引用 —— 这就是闭包。
    • count 并不会因为 outer() 的结束而被销毁。
  • 用途(优点)

    • 数据封装:创建私有变量,避免全局污染。

    • 模块化:隐藏内部实现,仅暴露必要接口,实现模块化编程。

    • 缓存:保存函数的计算结果,避免重复计算,提升性能。

  • 缺点

    • 内存泄漏风险:闭包持有外部变量引用,未合理释放可能导致内存无法回收。

    • 性能开销:大量闭包或深层嵌套,增加内存占用和执行成本。

    • 调试困难:变量作用域复杂,嵌套层级多时不易追踪问题。


11. var、 let、const 区别

特性varletconst
作用域函数作用域块级作用域块级作用域
是否变量提升是,初始化为 undefined是,但不初始化是,但不初始化
是否可重复声明可以不可以不可以
是否可重新赋值可以可以不可以(对象内部属性可变)
常见问题容易引发作用域污染、变量提升混乱更安全,用于局部变量用于声明常量
  • 作用域不同
    • var 声明的变量拥有函数作用域,如果在函数外声明,相当于全局变量。
    • letconst块级作用域(如 {} 花括号内有效),不会泄露到外部。
function testVar() {
  if (true) {
    var x = 1;
  }
  console.log(x); // 输出 1
}

function testLet() {
  if (true) {
    let y = 2;
  }
  console.log(y); // 报错:y is not defined
}
  • 变量提升不同
    • var 声明的变量会被提升到函数或全局顶部,但值是 undefined
    • letconst 也会被提升,但在实际赋值前不可访问(暂时性死区)。
console.log(a); // undefined
var a = 5;

console.log(b); // 报错
let b = 10;
  • 重新赋值与重复声明
    • var 可以重复声明赋值
    • let 可以赋值,但不允许重复声明
    • const 既不能重复声明也不能重新赋值(如果是对象,可以修改对象内部的属性)。
var c = 1;
var c = 2; // OK

let d = 1;
// let d = 2; // 报错

const e = 1;
// e = 2; // 报错

const obj = { name: "Alice" };
obj.name = "Bob";  // OK,对象内部属性可以改

const 保证的是绑定关系不可变,而不是对象本身的不可变。


12. this 对象是什么?

  • this :函数运行时指向调用它的对象,根据调用方式动态绑定的。

  • 常见的 this 指向情况:

    • 全局环境下(浏览器): 指向 window

      console.log(this === window); // true
      
    • 普通函数调用:指向 window(严格模式下是 undefined)。

      function foo() {
        console.log(this);
      }
      foo(); // window 或 undefined(严格模式)
      
    • 对象方法调用:如果函数作为对象的方法被调用,指向这个对象本身

      const obj = {
        name: "Tom",
        sayName: function() {
          console.log(this.name);
        }
      };
      obj.sayName(); // "Tom"
      
    • 构造函数调用(new):使用 new 调用函数时,指向新创建的对象实例

      function Person(name) {
        this.name = name;
      }
      const p = new Person("Alice");
      console.log(p.name); // "Alice"
      
    • 箭头函数:箭头函数没有自己的 this它继承自外层作用域this

      const obj = {
        name: "Tom",
        sayName: () => {
          console.log(this.name);
        }
      };
      obj.sayName(); // undefined (因为 this 指向 window)
      
    • 手动绑定 this(call, apply, bind):显式指定 this

      function greet() {
        console.log(this.name);
      }
      const user = { name: "Jack" };
      greet.call(user); // "Jack"
      greet.apply(user); // "Jack"
      const boundGreet = greet.bind(user);
      boundGreet(); // "Jack"
      

13. new 操作符的工作过程

  • 用途: 根据构造函数创建实例。

    • 创建新的空对象
    • 绑定原型,新对象的原型指向构造函数的原型对象。
    • 绑定 this,构造函数的 this 指向新对象。
    • 返回对象,如果构造函数返回对象则返回该对象,否则返回新创建的对象。
  • 自定义实现 new

function myNew(Constructor, ...args) {
  const obj = {}; // 创建空对象
  obj._proto_ = Constructor.prototype; // 绑定原型
  const result = Constructor.apply(obj, args); // 绑定this
  return result && result instanceof Object ? result : obj; // 返回对象
}
function Person(name) {
  this.name = name;
  // return { age: 20 }; // 显式返回对象,会覆盖 this
}

const p = new Person("Alice");
console.log(p); // {name: "Alice"} // { age: 20 }
console.log(p.name); // "Alice" // undifined

14. 箭头函数

  • 语法规则
语法形式示例
无参数() => 42
一个参数(可省略括号)x => x * x
多个参数(a, b) => a + b
多行函数体(需用 {}return(a, b) => { return a + b; }
返回对象(必须加括号)() => ({ name: "Alice" })
  • 特点

    • 没有原型(prototype)属性:不能用于构造函数,无法继承。
    const normal = function() {};
    const arrow = () => {};
    console.log(normal.prototype); // {}
    console.log(arrow.prototype); // undefined
    
    • 没有自己的 this:继承外层函数的 this,不会创建自己的 this
     const obj = {
       value: 10,
       getValue: function() {
         return this.value;
       }
     };
     console.log(obj.getValue()); // 10
     const objArrow = {
       value: 10,
       getValue: () => {
         return this.value;
       }
     };
     console.log(objArrow.getValue()); // undefined(不是 obj,而是全局或上层作用域)
    
    
    • 不能作为构造函数:不能使用 new 调用箭头函数创建实例,否则会报错。
    function Person(name) {
      this.name = name;
    }
    const p1 = new Person("Alice"); // 正常
    
    const PersonArrow = (name) => {
      this.name = name;
    };
    // const p2 = new PersonArrow("Bob"); // 报错
    
    • 没有 arguments 对象:如果需要参数信息,必须使用剩余参数 ...args
    function normal() {
      console.log(arguments);
    }
    normal(1, 2); // [1, 2]
    
    const arrow = (...args) => {
      console.log(args);
    };
    arrow(1, 2); // [1, 2]
    

15. 深拷贝与浅拷贝

区别概述

拷贝类型行为描述
浅拷贝拷贝数据的第一层属性,属性为引用类型,会复制引用地址,拷贝对象和原对象相互影响
深拷贝递归拷贝数据所有层级属性,属性为引用类型,会创建新的内存副本,拷贝对象和原对象互不影响

浅拷贝方法

  • 用于对象:Object.assign({}, obj){ ...obj }
const obj = { a: 1, b: { c: 2 } };
const shallow = Object.assign({}, obj)
// const shallow = { ...obj }; 

shallow.a = 0;
shallow.b.c = 99;

console.log(obj); // { a: 1, b: { c: 99 } }
console.log(shallow); // { a: 0, b: { c: 99 } }
  • 用于数组:slice()concat()from()[ ...array ]
const original = [1, [2], 3];
const copy = original.slice();
// const copy = [].concat(original);
// const copy = Array.from(original);
// const copy = [ ...original ];

copy[0] = 100;
copy[1][0] = 200;
console.log(original); // [1, [200], 3]
console.log(copy);     // [100, [200], 3]

深拷贝方法

  • JSON.parse(JSON.stringify(obj))
    • 不能拷贝undefinedSymbolFunction 等特殊值。
    • 循环引用(对象引用自身)会导致 JSON.stringify 报错。
const obj = { a: 1, b: { c: 2 } };
const deep = JSON.parse(JSON.stringify(obj));

deep.b.c = 99;
console.log(obj); // { a: 1, b: { c: 2 } }
  • 递归拷贝函数
function deepClone(obj, hash = new WeakMap()) {
  // 基本类型和函数直接返回
  if (obj === null || typeof obj !== "object") return obj;
  
  // 特殊类型处理
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);

  // 循环引用处理
  if (hash.has(obj)) return hash.get(obj);
  
  const clone = new obj.constructor();
  hash.set(obj, clone)
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key],hash);
    }
  }
  return clone;
}

// 例子
const a = {
  name: 'Alice',
  date: new Date(),
  reg: /test/i,
  meta: new Map([['key', { nested: true }]]),
  tags: new Set([1, 2, 3]),
  info: { age: 25 }
};
a.self = a; // 循环引用

const b = deepClone(a);

console.log(b);
console.log(b.self === b); // true(循环引用保留)
console.log(b.info !== a.info); // true(深拷贝成功)
console.log(b.meta.get('key') !== a.meta.get('key')); // true(深拷贝成功)
import cloneDeep from 'lodash/cloneDeep';
const newObj = cloneDeep(obj);

16. 判断对象是否为空

判断对象是否为空通常指检查对象是否没有自身的可枚举属性。以下是几种常用方法及其详细说明:

方法一:使用 Object.keys()

原理:将对象的键转为数组,判断数组长度是否为 0。
特点

  • 仅检查对象自身可枚举属性(不包含继承属性)。
  • 兼容性好,推荐使用。
function isEmpty(obj) {
  // 确保是普通对象(排除数组、null等非对象类型)
  return Object.prototype.toString.call(obj) === '[object Object]' && 
         Object.keys(obj).length === 0;
}

// 示例
isEmpty({});           // true
isEmpty({ a: 1 });     // false
isEmpty(null);         // false(非对象)
isEmpty([]);           // false(数组非普通对象)

方法二:使用 for...in 循环

原理:遍历对象属性,发现任何可枚举属性立即返回 false
特点

  • 会遍历继承的可枚举属性,需配合 hasOwnProperty 过滤。
  • 性能略低,适用于需要兼容极旧环境。
function isEmpty(obj) {
  if (Object.prototype.toString.call(obj) !== '[object Object]') return false;
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) return false;
  }
  return true;
}

方法三:使用 JSON.stringify()

原理:将对象转为 JSON 字符串,判断结果是否为 "{}"
缺点

  • 忽略 undefined、函数、Symbol 等值。
  • 无法处理不可枚举属性和循环引用。
  • 不推荐用于严格判断。
function isEmpty(obj) {
  return JSON.stringify(obj) === '{}';
}

// 问题示例
isEmpty({ key: undefined });  // true(误判)

方法四:使用 Reflect.ownKeys()

原理:获取对象所有自身属性(包括 Symbol 和不可枚举)。
适用场景:需要严格检查是否存在任何自身属性。

function isEmpty(obj) {
  return Object.prototype.toString.call(obj) === '[object Object]' && 
         Reflect.ownKeys(obj).length === 0;
}

方法对比表

方法自身可枚举属性继承属性不可枚举属性Symbol 键性能推荐度
Object.keys()⭐⭐⭐⭐
for...in✅ (需过滤)⭐⭐
JSON.stringify()✅ (部分忽略)
Reflect.ownKeys()⭐⭐⭐

17. for...in 和 for...of 的区别

for...infor...of
遍历内容遍历对象的可枚举属性名(包括原型链)遍历数组或类数组对象的(如数组元素、字符串字符)
适用对象普通对象、数组(但会遍历索引和自定义属性)可迭代对象(数组、字符串、Map、Set等)

for...in

  • 遍历对象的键名
    循环遍历对象自身的和继承的可枚举属性(通过 enumerable: true 定义)。

  • 过滤继承属性
    通常需配合 hasOwnProperty 检查属性是否属于对象自身:

  • 遍历数组时的陷阱
    会遍历数组的索引(字符串类型)和手动添加的自定义属性

for...of

  • 遍历可迭代对象的值
    直接获取数组元素、字符串字符等值,不会遍历索引或自定义属性:

  • 仅适用于可迭代对象
    可迭代对象必须实现 Symbol.iterator 方法。普通对象默认不可迭代

遍历数组

const arr = [10, 20, 30];
arr.customProp = "foo";

// for...in(遍历索引)
for (const index in arr) {
  console.log(index);        // "0", "1", "2", "customProp"(字符串类型)
  console.log(arr[index]);   // 10, 20, 30, "foo"
}

// for...of(遍历值)
for (const value of arr) {
  console.log(value);        // 10, 20, 30
}

遍历字符串

const str = "hello";

// for...in(遍历索引)
for (const index in str) {
  console.log(index);        // "0", "1", "2", "3", "4"
  console.log(str[index]);   // "h", "e", "l", "l", "o"
}

// for...of(遍历字符)
for (const char of str) {
  console.log(char);         // "h", "e", "l", "l", "o"
}

遍历对象(需手动处理)

const obj = { a: 1, b: 2 };
obj.__proto__.c = 3; 

// for...in(遍历键)
for (const key in obj) {
  console.log(key); // "a", "b", "c"
  console.log(obj[key]); // 1, 2, 3 
  if(obj.hasOwnProperty(key)) {
  console.log(key, obj[key]); // "a 1", "b 2"
  }
}

// for...of(需先转换为可迭代对象)
for (const value of Object.values(obj)) {
  console.log(value); // 1, 2
}

18. Map 和 Set

一、Map(映射)

Map 键值对集合,键和值都支持任何类型的键,保留插入顺序。

核心特性
  • 键的多样性:键可以是任意数据类型(对象、函数、原始值等),而 Object 的键只能是字符串或 Symbol。
  • 顺序性:插入顺序被保留,遍历时按插入顺序返回键值对。
  • 高效性:查找、插入、删除操作的时间复杂度接近 O(1)(基于哈希表实现)。
  • 可迭代性:直接支持 for...offorEach 等迭代方法。
常用方法
方法/属性作用示例
new Map()创建空 Mapconst map = new Map();
map.set(key, value)添加键值对(支持链式调用)map.set('a', 1).set('b', 2);
map.get(key)获取键对应的值(不存在返回 undefinedmap.get('a'); // 1
map.has(key)判断是否包含某键map.has('a'); // true
map.delete(key)删除指定键的键值对map.delete('a'); // true
map.clear()清空所有键值对map.clear();
map.size返回键值对的数量map.size; // 2
迭代方法
const map = new Map([['a', 1], ['b', 2]]);

// 遍历键值对
for (const [key, value] of map) {
  console.log(key, value); // 'a 1', 'b 2'
}

// 遍历键
for (const key of map.keys()) {
  console.log(key); // 'a', 'b'
}

// 遍历值
for (const value of map.values()) {
  console.log(value); // 1, 2
}

// 使用 forEach
map.forEach((value, key) => {
  console.log(key, value);
});

二、 Set(集合)

Set 是一种唯一值的集合,自动去重,保留插入顺序。

核心特性
  • 唯一性:集合中的值唯一(基于 === 判断,但 NaN 视为相等)。
  • 顺序性:插入顺序被保留。
  • 高效性:检查值是否存在的时间复杂度接近 O(1)
  • 可迭代性:支持 for...offorEach 等迭代方法。
常用方法
方法/属性作用示例
new Set()创建空 Setconst set = new Set();
set.add(value)添加值(支持链式调用)set.add(1).add(2);
set.has(value)判断是否包含某值set.has(1); // true
set.delete(value)删除指定值set.delete(1); // true
set.clear()清空所有值set.clear();
set.size返回值的数量set.size; // 2
迭代方法
const set = new Set([1, 2, 3]);

// 遍历值
for (const value of set) {
  console.log(value); // 1, 2, 3
}

// 使用 forEach
set.forEach((value) => {
  console.log(value);
});

三、Map vs ObjectSet vs Array 对比

MapObject 对比

特性MapObject
键类型任意类型字符串或 Symbol
键顺序保留插入顺序ES6 后字符串键按插入顺序
默认键从原型链继承(如 toString
性能频繁增删时更优适合静态键值对
序列化无法直接 JSON.stringify可直接序列化

SetArray 对比

特性SetArray
唯一性自动去重允许重复元素
查找效率O(1)O(n)
顺序性保留插入顺序保留插入顺序
API 丰富性简单丰富(如 mapfilter

19. 同源策略和跨域请求实现方法

同源:两个 URL 的 协议(Protocol)、域名(Host)、端口(Port) 完全相同。

  • https://example.com vs http://example.com不同源(协议不同)
  • https://example.com:443 vs https://example.com:8080不同源(端口不同)

同源策略:浏览器限制不同源的文档或脚本之间的交互,防止恶意网站窃取数据。

受限制的操作包括:

  • DOM 访问:禁止跨域访问 iframe 内容(如 document.getElementById)。
  • 数据存储:限制访问非同源的 CookieLocalStorage
  • 网络请求:默认禁止通过 XMLHttpRequestFetch 发起跨域请求。

跨域请求实现方法

  • CORS(跨域资源共享)(推荐的主流方法)
    原理:服务器通过设置 HTTP 响应头,显式允许某些源的请求。
    适用场景:现代浏览器的主流跨域解决方案,需服务端配合。

  • JSONP(JSON with Padding)(旧浏览器)
    原理:利用 <script> 标签不受同源策略限制的特性,通过回调函数获取数据。
    特点:仅支持 GET 请求,存在安全风险(如 XSS)。

  • 代理服务器
    原理:在同源服务器上设置代理,前端请求代理,代理转发请求到目标服务器。
    适用场景:适用于无法修改目标服务器配置的情况。

    // 前端请求同源代理
    fetch('/proxy?url=https://api.example.com/data')
      .then(response => response.json());
    
    // 服务端(Node.js 示例)
    app.get('/proxy', async (req, res) => {
      const targetUrl = req.query.url;
      const response = await fetch(targetUrl);
      res.send(await response.text());
    });
    
  • WebSocket
    原理:WebSocket 协议不受同源策略限制,可直接跨域通信。
    示例

    const socket = new WebSocket('wss://api.example.com/socket');
    socket.onmessage = (event) => {
      console.log('Message:', event.data);
    };
    
  • postMessage (窗口间通信)
    原理:HTML5 提供的跨文档通信 API,允许不同源窗口间安全传递消息。
    示例

    // 发送方(例如 iframe 内部)
    window.parent.postMessage('Hello from iframe!', 'https://parent-site.com');
    
    // 接收方(父页面)
    window.addEventListener('message', (event) => {
      if (event.origin === 'https://child-site.com') {
        console.log('Received:', event.data);
      }
    });
    
  • 修改 document.domain
    仅适用于主域相同、子域不同的场景(如 a.example.comb.example.com 均设为 example.com)。

  • 反向代理:通过 Nginx 等工具配置反向代理,将跨域请求转换为同源请求。


20. 哪些操作会导致内存泄漏?

  • 未使用 var/let/const 声明的变量:会隐式创建全局变量,长时间驻留内存。

  • 闭包:不当使用闭包可能导致外部变量无法被回收,造成内存泄漏。

  • 循环引用:两个对象相互引用, 垃圾回收无法判断它们是否还在使用,导致无法释放。

  • 未清理的事件监听:移除 DOM 元素但未解绑事件,特别是在 IE 中常见。

  • 滥用 console.log:开发工具可能会保留对日志中对象的引用,阻止垃圾回收。

  • setTimeout 第一个参数为字符串:类似 eval 的行为,创建作用域链,可能增加内存负担。


21. 什么是事件循环(Event Loop)?

JavaScript 是一门单线程(同一时间只能做一件事)、非阻塞语言,依靠事件循环机制实现异步操作

  • 任务执行模型分为
    • 同步任务:直接进入主线程,按顺序执行。

    • 异步任务:如 AJAX 网络请求、setTimeout 定时器等,会被加入任务队列,等待主线程空闲后执行。

      • 宏任务与微任务
      类型示例执行时机
      微任务Promise.then/.catch/.finallyMutationObserver同步任务执行完成后立即执行
      宏任务setTimeoutsetInterval、UI 事件(点击、滚动)微任务完成后执行
  • 事件循环流程
    • 主线程执行同步任务。
    • 遇到异步任务,将其挂起,注册回调。
    • 同步任务执行完毕后,从任务队列取出异步任务。
    • 异步任务进入主线程执行。
    • 重复此流程 —— 这就是事件循环。

22. Promise 详解

Pomise

  • 表示异步任务状态结果的对象,解决了传统回调函数嵌套(回调地狱)的问题。

状态

  • Pending(进行中):初始状态,异步操作尚未完成。
  • Fulfilled(已成功):异步操作成功完成,返回结果。
  • Rejected(已失败):异步操作失败,返回错误原因。

状态一旦改变(从 Pending → Fulfilled 或 Pending → Rejected),便不可再修改。
通过 new Promise() 构造函数创建 Promise 实例,接收一个执行器函数(executor):

const promise = new Promise((resolve, reject) => {
// 异步操作(如网络请求、定时器等)
setTimeout(() => {
const success = true;
if (success) {
  resolve("操作成功!"); // 状态变为 Fulfilled
} else {
  reject("操作失败!");  // 状态变为 Rejected
}
}, 1000);
});

静态方法

  • Promise.all():并行执行,全部成功时返回结果数组,任意失败则立即终止并返回失败。
  • Promise.allSettled():并行执行,全部完成后返回结果数组,包含状态 + 成果值/失败原因。
  • Promise.race():返回第一个完成(成功或失败)的 Promise。
  • Promise.any():返回第一个成功的 Promise,若全部失败,则抛出异常。

特性

  • 支持链式调用(.then()成功结果 / .catch()失败结果/.finally(无论成功或失败都执行))
  • 错误集中处理
  • 组合式调用实现复杂逻辑

async/await 简介

  • async 函数返回一个 Promise。
  • await 表达式用于等待一个 Promise 的结果。
  • 用法简洁、可读性强,让异步代码像同步一样书写。
async function fetchData() {
  try {
    const result = await fetch(url);
    const data = await result.json();
    console.log(data);
  } catch (err) {
    console.error(err);
  }
}

23. if...else 条件判断使用分析

if...else 缺点

问题描述
❌ 可读性差多层嵌套让代码不清晰,不易阅读
❌ 不易扩展每次加分支都要改源码,易引发错误
❌ 不易测试条件复杂时不容易单元测试
❌ 违反开闭原则逻辑写死在函数里,新增逻辑需“修改”而不是“扩展”

优化方案

场景推荐方案
少量判断、简单逻辑if...else / 三元运算符
多分支,进行选择操作switch
多值映射/多行为调用对象字典映射
行为需扩展多态 / 策略模式
1. 三元运算符(仅适用于简单条件)
const result = age > 18 ? 'Adult' : 'Minor'
2. 使用 switch(适合多分支,进行选择操作)
switch (status) {
  case 'success':
    handleSuccess()
    break
  case 'fail':
    handleFail()
    break
  default:
    handleDefault()
}
3. 使用对象字典映射表(根据变量选择对应行为/值)
const actions = {
  admin: 'Admin Panel',
  user: 'User Dashboard',
  guest: 'Login Page',
}

const role = 'user'
const result = actions[role] || ''
console.log(result) // User Dashboard
const PaymentStrategy = {
  alipay: () => console.log('支付宝支付'),
  wechat: () => console.log('微信支付'),
  credit: () => console.log('信用卡支付')
}

function pay(type) {
  const fn = PaymentStrategy[type]
  if (!fn) throw new Error('不支持该支付方式')
  fn()
}
pay('wechat')
4. 多态(面向对象,可扩展的行为封装场景)
class Animal {
  speak() { console.log('I make a sound') }
}
class Dog extends Animal {
  speak() { console.log('Woof') }
}
class Cat extends Animal {
  speak() { console.log('Meow') }
}

function makeAnimalSpeak(animal) {
  animal.speak()
}
makeAnimalSpeak(new Dog())