前端面试准备指南二之JavaScript核心篇


一、数据类型与类型检测

1.1 基本数据类型

  • string:字符串
  • number:数字(包含整数和浮点数)
  • boolean:布尔值(true/false)
  • undefined:未初始化的变量
  • null:空对象引用
  • symbol:ES6 引入,表示独一无二的值
  • bigint:ES2020 引入,表示任意精度整数

1.2 引用数据类型

  • object:普通对象
  • function:函数
  • array:数组
  • date:日期
  • regexp:正则表达式

1.3 typeof 操作符

返回值

返回值 描述
"undefined" 未定义
"boolean" 布尔值
"string" 字符串
"number" 数字
"bigint" 大整数
"symbol" 符号
"function" 函数
"object" 对象或 null

特殊情况

typeof null === "object"; // 历史bug,null被认为是空对象
typeof function () {} === "function"; // 函数单独一种类型

1.4 instanceof 操作符

原理:判断对象的原型链上是否存在某个构造函数的 prototype

[] instanceof Array;        // true
[] instanceof Object;       // true
{} instanceof Object;       // true
"str" instanceof String;    // false(字面量不是对象)

1.5 类型转换

显式类型转换

  • Number():转换为数字
  • String():转换为字符串
  • Boolean():转换为布尔值
  • parseInt():解析为整数
  • parseFloat():解析为浮点数

隐式类型转换

  • 算数运算符 -*/% 会转换为数字
  • 比较运算符 == 会进行类型转换
  • 逻辑运算符 !&&|| 会转换为布尔值

示例

"5" + 3; // "53"(字符串拼接)
"5" - 3; // 2(数学运算)
"5" == 5; // true(宽松相等)
"5" === 5; // false(严格相等)

二、作用域、作用域链与变量提升

2.1 作用域

作用域是一套规则,用于确定在何处以及如何查找变量。它负责管理变量的可访问性和生命周期。

作用域类型

类型 说明
全局作用域 代码运行前创建,页面关闭时销毁
函数作用域 函数内部定义的变量
块级作用域 let/const 限定在 {}

2.2 作用域链

从当前作用域开始一层一层向上寻找某个变量,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链

var name = "global";

function outer() {
    var name = "outer";

    function inner() {
        var name = "inner";
        console.log(name); // "inner"
    }

    inner();
}

outer();

2.3 变量提升

变量提升是指变量声明会被提升到作用域的顶部,但赋值不会被提升。

var 声明提升

console.log(name); // undefined(变量提升,但未赋值)
var name = "xiaoming";

// 预解析后相当于:
// var name;
// console.log(name);
// name = "xiaoming";

function 声明提升

fn(); // "Hello"
function fn() {
    console.log("Hello");
}

// 预解析后相当于:
// function fn() { console.log("Hello"); }
// fn();

let 和 const 暂时性死区

// console.log(name); // ReferenceError: Cannot access 'name' before initialization
let name = "xiaoming";

// const 必须初始化
// const age; // SyntaxError: Missing initializer in const declaration
const age = 20;

三、预解析机制

3.1 预解析概念

在 JavaScript 代码执行前,JS 引擎会先对代码进行预解析,将变量声明和函数声明提升到当前作用域的顶部。

3.2 声明与定义

声明(declare):只是告诉浏览器这有一个变量

var name; // 声明
function fn() {} // 声明

定义(defined):给声明的变量赋值

name = "xiaoming"; // 定义
fn = function () {}; // 定义

3.3 var 函数提升

var 在预解析阶段只是提前声明了变量,只有当代码执行的时候才会赋值,默认值是 undefined

console.log(a); // undefined
var a = 1;
console.log(a); // 1

// 预解析后相当于:
// var a;
// console.log(a);
// a = 1;

3.4 function 完全提升

function 在预解析的时候会提前把声明和定义都完成了。

b(); // "Hello"
var b = function () {
    console.log("Hello");
};
// 预解析后相当于:
// var b;
// b(); // 报错:b is not a function
// b = function(){...}

3.5 优先级

函数声明 > var 声明 > let/const 声明

fn(); // "function"
var fn;
function fn() {
    console.log("function");
}
fn = "var";
fn(); // TypeError: fn is not a function

四、闭包

4.1 闭包定义

闭包是指有权访问另一个函数作用域中的变量的函数。 ——《JavaScript高级程序设计》

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。 ——《你不知道的JavaScript》

4.2 闭包产生条件

  1. 函数嵌套
  2. 内部函数引用外部函数的变量
  3. 内部函数被外部引用

4.3 闭包用途

  • 访问外部变量:能够访问函数定义时所在的词法作用域
  • 私有化变量:创建私有变量和私有方法
  • 模拟块级作用域:在 ES6 之前模拟块级作用域
  • 创建模块:模块化开发

示例

function counter() {
    let count = 0; // 私有变量

    return {
        increment: function () {
            count++;
        },
        decrement: function () {
            count--;
        },
        getCount: function () {
            return count;
        },
    };
}

const counter1 = counter();
counter1.increment();
counter1.increment();
console.log(counter1.getCount()); // 2

4.4 闭包缺点

  • 函数的变量一直保存在内存中,过多的闭包会导致内存泄漏
  • 解决:及时释放不再使用的闭包引用

五、内存管理

5.1 垃圾回收机制

JavaScript 自动管理内存,垃圾回收器会回收不再使用的内存。

标记清除算法(常用):

  1. 垃圾回收器标记所有可达对象
  2. 清除未被标记的对象
  3. 回收内存空间

引用计数算法(较少使用):

  1. 记录对象被引用的次数
  2. 引用数为 0 时回收
  3. 缺点:无法处理循环引用

5.2 内存泄漏

常见原因

  • 闭包未释放
  • 全局变量过多
  • 事件监听器未移除
  • setTimeout/setInterval 未清除
  • 分离的 DOM 引用

5.3 内存优化建议

  • 及时释放不再使用的资源
  • 避免创建不必要的全局变量
  • 移除不再使用的事件监听器
  • 使用 WeakMap/WeakSet

六、原型与原型链

6.1 原型

对象中固有的 __proto__ 属性,该属性指向其构造函数的 prototype。每个构造函数都有一个 prototype 属性,指向一个对象,用于存放共享属性和方法。

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

Person.prototype.sayHello = function () {
    console.log("Hello, I'm " + this.name);
};

const p1 = new Person("Alice");
const p2 = new Person("Bob");

console.log(p1.__proto__ === Person.prototype); // true

6.2 原型链

当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,就会去它的原型对象里找,这个原型对象又会有自己的原型,就这样一直找下去,形成原型链。于是就这样一直找下去,也就是原型链的概念。

原型链终点Object.prototype.__proto__ === null,所以这就是我们新建的对象为什么能够使用 toString() 等方法的原因。

const obj = {};
console.log(obj.toString()); // "[object Object]"

// 原型链:obj -> Object.prototype -> null

6.3 原型特点

  • JavaScript 对象是通过引用来传递的
  • 创建的新对象实体并没有一份属于自己的原型副本
  • 修改原型时,与之相关的对象会继承这一改变

6.4 属性查找

function Parent() {
    this.parentValue = "parent";
}

Parent.prototype.parentMethod = function () {
    console.log("parent method");
};

function Child() {
    this.childValue = "child";
}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

const child = new Child();
child.childMethod(); // "child method"(自有属性)
child.parentMethod(); // "parent method"(原型链)
child.parentValue; // "parent"(原型链)

七、this指向与new关键字

7.1 this指向

this 是执行上下文的一个属性,指向最后一次调用这个方法的对象。

非严格模式 vs 严格模式

模式 全局函数中this 严格模式下
非严格模式 window/global this为undefined

当函数被作为某个对象的方法调用时,this 等于那个对象。在实际开发中,this 的指向可以通过四种调用模式来判断。

四种调用模式

模式 this指向 示例
函数调用 当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象(window/global) fn()
方法调用 如果一个函数作为一个对象的方法来调用时,this 指向这个对象 obj.fn()
构造函数调用 this 指向这个用 new 新创建的对象 new Fn()
apply/call/bind 这三个方法可以显式指定调用函数的 this 指向 fn.call(obj)

示例

// 函数调用
function fn() {
    console.log(this); // window(在非严格模式)
}
fn();

// 方法调用
const obj = {
    name: "obj",
    fn: function () {
        console.log(this.name); // "obj"
    },
};
obj.fn();

// 构造函数
function Person(name) {
    this.name = name;
}
const p = new Person("Alice");
console.log(p.name); // "Alice"

7.2 new关键字作用

  1. 首先创建了一个新的空对象
  2. 设置原型,将对象的原型设置为函数的 prototype 对象
  3. 让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象

7.3 call、apply、bind

相同点:都是用来重定义 this 这个对象的;第一个参数都是 this 要指向的对象;都可以利用后续参数传参;
不同点callapply 都是对函数的直接调用(也叫直接执行函数),而 bind 方法返回的是一个新的函数,因此后面还需要()来进行调用才可以;callbind 后面的参数与 fn 方法一一对应,apply 的第二个参数是一个数组;

方法 调用方式 参数 返回值
call fn.call(thisArg, arg1, arg2…) 逐一传递 立即执行
apply fn.apply(thisArg, [argsArray]) 数组传递 立即执行
bind fn.bind(thisArg, arg1, arg2…) 逐一传递 返回新函数

示例

function fn(a, b) {
    console.log(this.value, a + b);
}

const obj = { value: "obj" };

fn.call(obj, 1, 2); // "obj 3"
fn.apply(obj, [1, 2]); // "obj 3"
fn.bind(obj, 1, 2)(); // "obj 3"

八、继承方式

8.1 原型链继承

原理:通过将子类型的原型设置为超类型的实例来实现继承

缺点

  • 包含引用类型数据的属性会被所有实例对象共享,容易造成修改混乱
  • 在创建子类型的时候不能向超类型传递参数
function Parent() {
    this.colors = ["red", "blue"];
}

function Child() {}

Child.prototype = new Parent();

const child1 = new Child();
child1.colors.push("green");
console.log(child1.colors); // ["red", "blue", "green"]

8.2 借用构造函数

原理:通过在子类型的函数中调用超类型的构造函数来实现的

优点:解决了不能向超类型传递参数的缺点

缺点:无法实现函数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到

function Parent(name) {
    this.name = name;
}

function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}

const child = new Child("Alice", 20);

8.3 组合继承

原理:组合继承是将原型链和借用构造函数组合起来使用的一种方式。通过借用构造函数的方式来实现类型的属性的继承,通过将子类型的原型设置为超类型的实例来实现方法的继承

优点:解决了原型链继承和借用构造函数继承单独使用时的缺点

缺点:由于我们是以超类型的实例来作为子类型的原型,调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性

function Parent(name) {
    this.name = name;
    this.colors = ["red"];
}

Parent.prototype.sayName = function () {
    console.log(this.name);
};

function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

8.4 原型式继承

原理:基于已有的对象来创建新的对象,实现的原理是,向函数中传入一个对象,然后返回一个以这个对象为原型的对象。ES5 中定义的 Object.create() 方法就是原型式继承的实现

缺点:与原型链方式相同,引用类型属性被共享

const person = {
    name: "Alice",
    friends: ["Bob", "Charlie"],
};

const person1 = Object.create(person);
person1.name = "David";
person1.friends.push("Eve");
console.log(person.friends); // ["Bob", "Charlie", "Eve"]

8.5 寄生式继承

原理:创建一个用于封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,对象进行扩展,最后返回这个对象

优点:对一个简单对象实现继承

缺点:没有办法实现函数的复用

function createAnother(original) {
    const clone = Object.create(original);
    clone.sayHi = function () {
        console.log("Hi");
    };
    return clone;
}

8.6 寄生组合继承(最佳方案)

原理:使用超类型原型的副本来作为子类型的原型,这样就避免了创建不必要的属性

优点:解决了组合继承调用两次超类型构造函数的问题,是最理想的继承方式

function inheritPrototype(Parent, Child) {
    const prototype = Object.create(Parent.prototype);
    prototype.constructor = Child;
    Child.prototype = prototype;
}

function Parent(name) {
    this.name = name;
}

function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}

inheritPrototype(Parent, Child);

8.7 ES6 Class继承

原理:通过 classextends 实现继承,是ES6推荐的继承方式

class Parent {
    constructor(name) {
        this.name = name;
    }

    sayName() {
        console.log(this.name);
    }
}

class Child extends Parent {
    constructor(name, age) {
        super(name);
        this.age = age;
    }
}

const child = new Child("Alice", 20);

九、箭头函数

9.1 基本语法

// 传统函数
const add = function (a, b) {
    return a + b;
};

// 箭头函数
const add = (a, b) => a + b;

// 单参数可省略括号
const double = x => x * 2;

// 多行函数体需要 return
const calc = (a, b) => {
    const sum = a + b;
    return sum;
};

9.2 与普通函数区别

  1. 箭头函数是匿名函数,不能作为构造函数,不能使用new
  2. 箭头函数不绑定 arguments,取而代之用 rest 参数...解决
  3. 箭头函数不绑定 this,会捕获其所在的上下文的 this 值,作为自己的 this
  4. 箭头函数通过 call()apply() 方法调用一个函数时,只传入了一个参数,对 this 并没有影响。
  5. 箭头函数没有原型属性
  6. 箭头函数不能当做 Generator 函数,不能使用 yield 关键字

9.3 this指向

  • 箭头函数的 this 永远指向其上下文的 this ,任何方法都改变不了其指向,如call() , bind() , apply()
  • 普通函数的 this 指向调用它的那个对象,箭头函数的 this 指向其所在的上下文的 this 值,不能指向调用它的那个对象
const obj = {
    name: "obj",
    fn: function () {
        // 普通函数,this 指向 obj
        console.log(this.name); // "obj"
    },
    arrowFn: () => {
        // 箭头函数,this 指向上层(全局)
        console.log(this.name); // undefined
    },
};

9.4 不能使用的场景

  • 对象方法
  • 构造函数
  • 事件回调(this 丢失)
  • 原型方法

十、函数柯里化

10.1 柯里化定义

将一个函数的多个参数转换为只接受第一个参数的函数,每个参数都返回一个新函数。

10.2 柯里化优点

  • 参数复用:复用初始参数
  • 提前确认:提前处理配置
  • 延迟运行:延迟执行

10.3 实现原理

function currying(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        } else {
            return function (...args2) {
                return curried.apply(this, args.concat(args2));
            };
        }
    };
}

// 示例
function add(a, b, c) {
    return a + b + c;
}

const curriedAdd = currying(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6

十一、数组去重

11.1 ES6 Set 去重

利用ES6中的Set去重,将数组传入到Set方法中,然后转换回数组。

const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = [...new Set(arr)];
// 或
const uniqueArr = Array.from(new Set(arr));

console.log(uniqueArr); // [1, 2, 3, 4, 5]

11.2 for循环 + splice

拿每个元素与后面元素进行对比,如果有相同就删除后面重复的元素。

const arr = [1, 2, 2, 3, 4, 4, 5];
for (let i = 0; i < arr.length; i++) {
    for (let j = i + 1; j < arr.length; j++) {
        if (arr[i] === arr[j]) {
            arr.splice(j, 1);
            j--;
        }
    }
}

console.log(arr); // [1, 2, 3, 4, 5]

11.3 indexOf

创建一个空数组,然后遍历原数组,取出每个值,进行indexOf判断,如果等于-1表示不存在,加入到新数组中。

const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = [];
for (let i = 0; i < arr.length; i++) {
    if (uniqueArr.indexOf(arr[i]) === -1) {
        uniqueArr.push(arr[i]);
    }
}

console.log(uniqueArr); // [1, 2, 3, 4, 5]

11.4 sort +相邻比较

先排序,然后两两相邻比较,如果不相等就加入到新数组中。

const arr = [1, 2, 2, 3, 4, 4, 5];
const sortedArr = arr.sort();
const uniqueArr = [sortedArr[0]];
for (let i = 1; i < sortedArr.length; i++) {
    if (sortedArr[i] !== sortedArr[i - 1]) {
        uniqueArr.push(sortedArr[i]);
    }
}

console.log(uniqueArr); // [1, 2, 3, 4, 5]

11.5 includes

创建一个空数组,然后遍历原数组,利用includes判断新数组中是否存在该元素,如果不存在就添加到新数组中。

const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = [];
for (let i = 0; i < arr.length; i++) {
    if (!uniqueArr.includes(arr[i])) {
        uniqueArr.push(arr[i]);
    }
}

console.log(uniqueArr); // [1, 2, 3, 4, 5]

11.6 filter + indexOf

利用filter方法,判断每个元素的索引是否等于第一个出现的索引,如果等于就加入到新数组中。

const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = arr.filter((item, index) => arr.indexOf(item) === index);

console.log(uniqueArr); // [1, 2, 3, 4, 5]

十二、浅拷贝与深拷贝

12.1 概念区分

类型 说明
浅拷贝 只复制对象的引用,修改新对象会影响原对象
深拷贝 复制对象的所有层级,修改新对象不影响原对象

12.2 浅拷贝方法

for循环:遍历对象,将属性名和属性值赋值给新对象。

const obj = { a: 1, b: { c: 2 } };
const shallowCopy = {};
for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
        shallowCopy[key] = obj[key];
    }
}

Object.assign():将所有可枚举属性从一个或多个源对象复制到目标对象。

const obj = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, obj);

ES6对象扩展运算符:通过扩展运算符实现浅拷贝。

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

12.3 深拷贝方法

递归拷贝:递归遍历对象的所有层级,复制每个属性。

function deepClone(obj) {
    if (!obj || typeof obj !== "object") return obj;

    const newObj = Array.isArray(obj) ? [] : {};
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = typeof obj[key] === "object" ? deepClone(obj[key]) : obj[key];
        }
    }
    return newObj;
}

const obj = { a: 1, b: { c: 2 } };
const deepCopy = deepClone(obj);

JSON序列化:通过JSON序列化和反序列化实现深拷贝。

const obj = { a: 1, b: { c: 2 } };
const deepCopy = JSON.parse(JSON.stringify(obj));
// 缺点:无法拷贝函数、undefined、Symbol、Date、正则等

第三方库

// 使用lodash.cloneDeep()实现深拷贝
const _ = require("lodash");
const obj = { a: 1, b: { c: 2 } };
const deepCopy = _.cloneDeep(obj);

// 使用jQuery 里的 $.extend()方法实现深拷贝
const obj = { a: 1, b: { c: 2 } };
const deepCopy = $.extend(true, {}, obj);

12.4 注意事项

  • Object.assign() 一级属性是深拷贝,二级及以上是浅拷贝
  • 循环引用会导致递归爆栈,需要处理

十三、ES6新特性

13.1 Symbol

表示独一无二的值,用来定义独一无二的对象属性名。每个 Symbol 实例都是唯一的,其类型的key不能通过 Object.keys() 或者 for...in 来枚举,未被包含在对象自身的属性名集合之中。可以通过 Object.getOwnPropertySymbols(obj)Reflect.ownKeys(obj) 获取。

const sym = Symbol("description");
const obj = {
    [sym]: "value",
};

13.2 let/const

  • let:块级作用域,不存在变量提升(暂时性死区)
  • const:块级作用域,声明时必须初始化,不能重新赋值

13.3 解构赋值

支持数组、对象、字符串、数字及布尔值的解构,配合剩余运算符(...rest)使用

// 数组解构
const [a, b] = [1, 2];

// 对象解构
const { name, age } = { name: "Alice", age: 20 };

// 剩余运算符
const [first, ...rest] = [1, 2, 3];

13.4 模板字符串

:使用 ${data} 语法嵌入表达式

const name = "Alice";
const greeting = `Hello, ${name}!`;

13.5 扩展运算符

用于数组和对象的展开操作

// 数组展开
const arr1 = [1, 2];
const arr2 = [...arr1, 3, 4];

// 对象展开
const obj1 = { a: 1 };
const obj2 = { ...obj1, b: 2 };

13.6 Set/Map 数据结构

Set对象存储任何类型的唯一值,类似于数组但成员值都是唯一的

const set = new Set([1, 2, 2, 3]);
console.log([...set]); // [1, 2, 3]

Map对象保存键值对,任何值(对象或者原始值)都可以作为键或值,构造函数可接受数组作为参数

const map = new Map();
map.set("name", "Alice");
map.get("name"); // "Alice"

13.7 箭头函数

ES6 引入的箭头函数(=> { ... })语法,提供更简洁的函数写法,且不绑定自己的 this,不绑定 arguments,不能作为构造函数
基本语法

// 传统函数
const add = function (a, b) {
    return a + b;
};

// 箭头函数
const add = (a, b) => a + b;

// 单参数可省略括号
const double = x => x * 2;

// 多行函数体需要 return
const calc = (a, b) => {
    const sum = a + b;
    return sum;
};

与普通函数区别

特性 箭头函数 普通函数
this 继承外层上下文 指向调用者
arguments
构造函数 不能用 new 可以用 new
原型属性 没有

13.8 Proxy/Reflect

Proxy:用于创建对象的代理,Reflect 提供拦截 JavaScript 操作的方法

const proxy = new Proxy(target, {
    get(target, prop) {
        return Reflect.get(target, prop);
    },
    set(target, prop, value) {
        Reflect.set(target, prop, value);
    },
});

13.9 Class类

ES6 引入的类语法,基于原型继承的语法糖

class Person {
    #name; // 私有属性

    constructor(name) {
        this.#name = name;
    }

    greet() {
        console.log(`Hello, I'm ${this.#name}`);
    }
}

13.10 Module模块

使用 import/export 进行模块化开发

// 导出
export const name = "Alice";
export default function fn() {}

// 导入
import { name } from "./module.js";
import fn from "./module.js";

十四、ES7/ES8/ES9新特性

14.1 ES7特性

  • Array.prototype.includes():数组包含检查
  • 幂运算符 **2 ** 3 === 8

14.2 ES8特性

  • async/await:异步编程语法糖
  • Object.values():获取对象值数组
  • Object.entries():获取键值对数组
  • String.padStart()/padEnd():字符串填充

14.3 ES9特性

  • 异步迭代器 for await...of
  • Promise.finally()
  • 正则表达式改进
  • 对象展开运算符

十五、EventLoop事件循环

15.1 单线程与异步

JavaScript 是单线程的,为了防止函数执行时间过长阻塞后面的代码,采用事件循环机制。

执行顺序

  1. 同步代码压入执行栈依次执行
  2. 异步代码推入任务队列
  3. 执行栈清空后,读取任务队列中的任务,按顺序执行

15.2 任务队列

任务队列分为宏任务队列和微任务队列,微任务队列的任务会在当前宏任务执行完毕后、下一个宏任务开始前全部执行完毕。

类型 示例 执行时机
微任务 Promise.thenMutationObserver 当前宏任务执行完毕后,下一个宏任务开始前
宏任务 setTimeoutsetIntervalsetImmediate、I/O、UI渲染 同步栈清空后,下一轮事件循环

注意process.nextTick是Node.js环境特有的,不属于浏览器环境的微任务。

15.3 执行顺序示例

console.log("1"); // 同步

setTimeout(() => console.log("2"), 0); // 宏任务

Promise.resolve().then(() => console.log("3")); // 微任务

console.log("4"); // 同步

// 执行顺序:1 → 4 → 3 → 2

十六、异步编程

16.1 回调函数

在异步操作完成后调用的函数

function fetchData(callback) {
    setTimeout(() => {
        callback("data");
    }, 1000);
}

fetchData(data => {
    console.log(data); // "data"
});

16.2 Promise

解决回调地狱,提供链式调用

new Promise((resolve, reject) => {
    resolve("success");
})
    .then(value => {
        console.log(value); // "success"
        return value;
    })
    .catch(err => {
        console.error(err);
    });

16.3 async/await

基于 Promise 的语法糖,使异步代码看起来像同步代码

async function fetchData() {
    try {
        const response = await fetch(url);
        const data = await response.json();
        return data;
    } catch (err) {
        console.error(err);
    }
}

十七、原生Ajax

17.1 Ajax原理

一种异步通信的方法,从服务端获取数据,达到局部刷新页面的效果。

17.2 实现步骤

  1. 创建 XMLHttpRequest 对象
  2. 调用 open 方法(请求方式、URL、同步/异步)
  3. 监听 onreadystatechange 事件
  4. 调用 send 方法发送请求

17.3 示例

const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/data", true);
xhr.onreadystatechange = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(xhr.responseText);
    }
};
xhr.send();

// POST 请求
xhr.open("POST", "/api/submit", true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify({ name: "Alice" }));

十八、跨域请求

18.1 同源策略

浏览器的安全机制,限制一个域下的 JavaScript 脚本访问另一个域下的资源。

同源标准:协议、域名、端口都相同

18.2 CORS跨域

普通跨域:只需服务器设置 Access-Control-Allow-Origin

带Cookie跨域

// 前端设置
xhr.withCredentials = true;

// 服务端设置
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Credentials: true

18.2.1 原生ajax

var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容
// 前端设置是否带cookie
xhr.withCredentials = true;
xhr.open("post", "http://www.domain2.com:8080/login", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("user=admin");
xhr.onreadystatechange = function () {
    if (xhr.readyState == 4 && xhr.status == 200) {
        alert(xhr.responseText);
    }
};

18.2.2 jQuery ajax

$.ajax({
    url: "http://www.test.com:8080/login",
    type: "get",
    data: {},
    xhrFields: {
        withCredentials: true, // 前端设置是否带cookie
    },
    crossDomain: true, // 强制发送X-Requested-With头,与cookie无关
});

18.2.3 vue-resource

Vue.http.options.credentials = true;

18.2.4 axios

axios.defaults.withCredentials = true;

18.3 JSONP跨域

利用 <script> 标签无跨域限制的特性

<script src="http://test.com/data.php?callback=dosomething"></script>
<!-- 向服务器test.com发出请求,该请求的查询字符串有一个callback参数,用来指定回调函数的名字 -->
<!-- 处理服务器返回回调函数的数据 -->
<script type="text/javascript">
    function dosomething(res) {
        // 处理获得的数据
        console.log(res.data);
    }
</script>

18.4 JSONP jQuery实现

$.ajax({
    url: "http://www.test.com:8080/login",
    type: "get",
    dataType: "jsonp", // 请求方式为jsonp
    jsonpCallback: "handleCallback", // 自定义回调函数名
    data: {},
});

18.5 Vue.js 实现

this.$http
    .jsonp("http://www.domain2.com:8080/login", {
        params: {},
        jsonp: "handleCallback",
    })
    .then(res => {
        console.log(res);
    });

十九、DOM与BOM

DOM(Document Object Model) 是指文档对象模型,通过它,可以访问 HTML 文档的所有元素。DOMW3C(万维网联盟)的标准。DOM 定义了访问 HTMLXML 文档的标准:”W3C 文档对象模型(DOM)是中立于平台和语言的接口,它允许程序和脚本动态地访问和更新文档的内容、结构和样式。”

BOM(Browser Object Model) 是指浏览器对象模型,可以对浏览器窗口进行访问和操作。使用 BOM,开发者可以移动窗口、改变状态栏中的文本以及执行其他与页面内容不直接相关的动作。使 JavaScript 有能力与浏览器”对话”。

19.1 DOM(文档对象模型)

DOM节点类型

类型 说明
Element HTML元素节点
Text 文本节点
Comment 注释节点
Document 文档节点

常用操作

document.getElementById("id");
document.querySelector(".class");
element.appendChild(child);
element.removeChild(child);
element.setAttribute("attr", "value");
element.classList.add("active");

W3C DOM 标准被分为 3 个不同的部分

  • 核心 DOM - 针对任何结构化文档的标准模型
  • XML DOM - 针对 XML 文档的标准模型,定义了所有 XML 元素的对象和属性,以及访问它们的方法。
  • HTML DOM - 针对 HTML 文档的标准模型,定义了所有 HTML 元素的对象和属性,以及访问它们的方法。

19.2 BOM(浏览器对象模型)

window对象

  • window.innerWidth/innerHeight:视口尺寸
  • window.scrollX/scrollY:滚动位置
  • window.location:URL信息
  • window.history:浏览历史

navigator对象

  • navigator.userAgent:用户代理
  • navigator.platform:平台信息

screen对象

  • screen.width/height:屏幕尺寸
  • screen.availWidth/availHeight:可用尺寸

19.3 事件模型

事件传播

  1. 捕获阶段:从根节点向下传播
  2. 目标阶段:到达目标元素
  3. 冒泡阶段:从目标向上传播

事件委托:利用冒泡原理,将事件绑定到父元素

ul.addEventListener("click", e => {
    if (e.target.tagName === "LI") {
        console.log(e.target.textContent);
    }
});

19.4 事件对象属性

属性 说明
target 事件目标
currentTarget 绑定事件的元素
preventDefault() 阻止默认行为
stopPropagation() 阻止冒泡
type 事件类型

二十、事件冒泡、捕获与委托

  • 事件冒泡:在一个对象上触发某类事件,如果此对象绑定了事件,就会触发事件,如果没有,就会向这个对象的父级对象传播,最终父级对象触发了事件。
  • 事件捕获:从根节点开始向下传播到目标元素。
  • 事件委托:本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,并且父节点可以通过事件对象获取到目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为 事件代理
// 阻止事件冒泡
event.stopPropagation();
// 或者 IE下的方法
event.cancelBubble = true;

二十一、DOM操作优化

21.1 批量DOM操作

  • 使用DocumentFragment一次性插入多个节点
  • 先将元素脱离文档流,操作完成后再重新插入
// 使用DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
    const div = document.createElement("div");
    div.textContent = `Item ${i}`;
    fragment.appendChild(div);
}
container.appendChild(fragment);

21.2 避免频繁重排

  • 合并多次DOM样式修改
  • 使用requestAnimationFrame批量更新

二十二、防抖节流

函数防抖关注一定时间连续触发,只在最后执行一次,而函数节流侧重于一段时间内只执行一次。

22.1 防抖

// 定义:触发事件后延迟n秒再执行函数,如果在n秒内再次触发事件,则重新计时。
// 应用场景:搜索框搜索输入、手机号/邮箱验证输入检测、窗口大小Resize等
const debounce = (fn, wait, immediate) => {
    let timer = null;
    return function (...args) {
        if (timer) clearTimeout(timer);
        if (immediate && !timer) {
            fn.call(this, ...args);
        }
        timer = setTimeout(() => {
            fn.call(this, ...args);
            timer = null;
        }, wait);
    };
};

// 使用示例
const betterFn = debounce(() => console.log("fn 防抖执行了"), 1000, true);
document.addEventListener("scroll", betterFn);

22.2 节流

// 定义:当持续触发事件时,保证每隔一定时间触发一次事件。
// 应用场景:懒加载、滚动加载、加载更多、百度搜索框联想功能、防止高频点击提交等
function throttle(fn, wait) {
    let pre = 0;
    return function (...args) {
        let now = Date.now();
        if (now - pre >= wait) {
            fn.apply(this, args);
            pre = now;
        }
    };
}

// 使用示例
function handle() {
    console.log(Math.random());
}
window.addEventListener("mousemove", throttle(handle, 1000));

二十三、函数式编程

  • 纯函数:相同的输入总是产生相同的输出,没有副作用
  • 高阶函数:接受函数作为参数或返回函数
  • 函数组合:将多个函数组合成一个新函数
  • 柯里化:将多参数函数转换为单参数函数(详见 JS函数柯里化 章节)
  • 偏函数:固定函数的部分参数,返回一个新函数接受剩余参数
  • 不可变性:避免修改共享状态,数据一旦创建就不能改变

二十四、设计模式

  • 创建型模式:单例模式、工厂模式、构造函数模式、原型模式
  • 结构型模式:适配器模式、装饰器模式、代理模式
  • 行为型模式:观察者模式、发布-订阅模式、命令模式、策略模式

二十五、正则表达式

  • 基本语法:字符类、量词、锚点、分组
  • 常见用途:表单验证、字符串处理

二十六、错误处理

  • try-catch-finally
  • throw 语句
  • Error 对象及派生类

二十七、模块化开发

  • CommonJS:Node.js 使用的模块系统
  • AMD:RequireJS 使用的模块系统
  • CMD:SeaJS 使用的模块系统
  • ES6 Module:ES6 引入的标准模块系统

二十八、面试手写代码系列

以下是面试中常见的手写代码题,按照从简单到复杂的顺序排列,建议按顺序学习和练习。

28.1 数组去重(⭐ 简单)

28.1.1 数组去重

const arr = [2, 7, 5, 7, 2, 8, 9];

// 方法1:使用Set
console.log([...new Set(arr)]); // [2, 7, 5, 8, 9]

// 方法2:使用filter
const uniqueArr = arr.filter((item, index) => arr.indexOf(item) === index);
console.log(uniqueArr); // [2, 7, 5, 8, 9]

28.1.2 数组对象去重

const list = [
    { age: 18, name: "张三" },
    { age: 18, name: "李四" },
    { age: 20, name: "王五" },
];

// 根据age去重
let hash = {};
const newArr = list.reduce((item, next) => {
    !hash[next.age] && (hash[next.age] = true) && item.push(next);
    return item;
}, []);
console.log(newArr);

28.2 数组扁平化(⭐⭐ 中等)

// 方法1:使用reduce
function flatten(arr) {
    return arr.reduce((result, item) => {
        return result.concat(Array.isArray(item) ? flatten(item) : item);
    }, []);
}

// 方法2:使用ES6的flat方法
const arr = [1, [2, [3, 4]]];
console.log(arr.flat(Infinity)); // [1, 2, 3, 4]

// 使用示例
const nestedArr = [1, [2, 3], [4, [5, 6]]];
console.log(flatten(nestedArr)); // [1, 2, 3, 4, 5, 6]

28.3 对象深浅拷贝(⭐⭐ 中等)

28.3.1 浅拷贝

// 方法1:Object.assign
const target = {};
const source = { a: 1, b: { c: 2 } };
Object.assign(target, source);

// 方法2:ES6对象扩展运算符
const newObj = { ...source };

28.3.2 深拷贝

function deepClone(obj) {
    if (!obj || typeof obj !== "object") return obj;
    let newObj = Array.isArray(obj) ? [] : {};
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = typeof obj[key] === "object" ? deepClone(obj[key]) : obj[key];
        }
    }
    return newObj;
}

// 使用示例
const obj = { a: 1, b: { c: 2, d: [3, 4] } };
const clonedObj = deepClone(obj);

28.4 实现EventEmitter(⭐⭐⭐ 中等偏难)

class EventEmitter {
    constructor() {
        this.events = {};
    }

    // 注册事件
    on(eventName, callback) {
        if (!this.events[eventName]) {
            this.events[eventName] = [];
        }
        this.events[eventName].push(callback);
    }

    // 触发事件
    emit(eventName, ...args) {
        if (this.events[eventName]) {
            this.events[eventName].forEach(callback => {
                callback(...args);
            });
        }
    }

    // 移除事件
    off(eventName, callback) {
        if (this.events[eventName]) {
            this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
        }
    }

    // 只触发一次事件
    once(eventName, callback) {
        const onceCallback = (...args) => {
            callback(...args);
            this.off(eventName, onceCallback);
        };
        this.on(eventName, onceCallback);
    }
}

// 使用示例
const emitter = new EventEmitter();

// 注册事件
emitter.on("test", msg => {
    console.log(`收到消息: ${msg}`);
});

// 触发事件
emitter.emit("test", "Hello EventEmitter");

// 只触发一次的事件
emitter.once("onceEvent", () => {
    console.log("这个事件只触发一次");
});
emitter.emit("onceEvent");
emitter.emit("onceEvent"); // 不会触发

28.5 Promise实现(⭐⭐⭐⭐⭐ 困难)

class MyPromise {
    constructor(executor) {
        this.status = "pending";
        this.value = undefined;
        this.reason = undefined;
        this.onFulfilledCallbacks = [];
        this.onRejectedCallbacks = [];

        const resolve = value => {
            if (this.status === "pending") {
                this.status = "fulfilled";
                this.value = value;
                this.onFulfilledCallbacks.forEach(callback => callback(value));
            }
        };

        const reject = reason => {
            if (this.status === "pending") {
                this.status = "rejected";
                this.reason = reason;
                this.onRejectedCallbacks.forEach(callback => callback(reason));
            }
        };

        try {
            executor(resolve, reject);
        } catch (error) {
            reject(error);
        }
    }

    then(onFulfilled, onRejected) {
        onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value;
        onRejected =
            typeof onRejected === "function"
                ? onRejected
                : reason => {
                      throw reason;
                  };

        const promise2 = new MyPromise((resolve, reject) => {
            if (this.status === "fulfilled") {
                setTimeout(() => {
                    try {
                        const x = onFulfilled(this.value);
                        this.resolvePromise(promise2, x, resolve, reject);
                    } catch (error) {
                        reject(error);
                    }
                }, 0);
            }

            if (this.status === "rejected") {
                setTimeout(() => {
                    try {
                        const x = onRejected(this.reason);
                        this.resolvePromise(promise2, x, resolve, reject);
                    } catch (error) {
                        reject(error);
                    }
                }, 0);
            }

            if (this.status === "pending") {
                this.onFulfilledCallbacks.push(() => {
                    setTimeout(() => {
                        try {
                            const x = onFulfilled(this.value);
                            this.resolvePromise(promise2, x, resolve, reject);
                        } catch (error) {
                            reject(error);
                        }
                    }, 0);
                });

                this.onRejectedCallbacks.push(() => {
                    setTimeout(() => {
                        try {
                            const x = onRejected(this.reason);
                            this.resolvePromise(promise2, x, resolve, reject);
                        } catch (error) {
                            reject(error);
                        }
                    }, 0);
                });
            }
        });

        return promise2;
    }

    resolvePromise(promise2, x, resolve, reject) {
        if (promise2 === x) {
            return reject(new TypeError("Chaining cycle detected for promise"));
        }

        if (x instanceof MyPromise) {
            x.then(
                value => {
                    this.resolvePromise(promise2, value, resolve, reject);
                },
                reason => {
                    reject(reason);
                }
            );
        } else if (typeof x === "object" && x !== null) {
            let called = false;
            try {
                const then = x.then;
                if (typeof then === "function") {
                    then.call(
                        x,
                        value => {
                            if (called) return;
                            called = true;
                            this.resolvePromise(promise2, value, resolve, reject);
                        },
                        reason => {
                            if (called) return;
                            called = true;
                            reject(reason);
                        }
                    );
                } else {
                    resolve(x);
                }
            } catch (error) {
                if (called) return;
                called = true;
                reject(error);
            }
        } else {
            resolve(x);
        }
    }

    static resolve(value) {
        return new MyPromise(resolve => {
            resolve(value);
        });
    }

    static reject(reason) {
        return new MyPromise((resolve, reject) => {
            reject(reason);
        });
    }

    static all(promises) {
        return new MyPromise((resolve, reject) => {
            const results = [];
            let count = 0;

            for (let i = 0; i < promises.length; i++) {
                promises[i].then(
                    value => {
                        results[i] = value;
                        count++;
                        if (count === promises.length) {
                            resolve(results);
                        }
                    },
                    reason => {
                        reject(reason);
                    }
                );
            }
        });
    }

    static race(promises) {
        return new MyPromise((resolve, reject) => {
            for (let i = 0; i < promises.length; i++) {
                promises[i].then(
                    value => {
                        resolve(value);
                    },
                    reason => {
                        reject(reason);
                    }
                );
            }
        });
    }
}

// 使用示例
const promise = new MyPromise((resolve, reject) => {
    setTimeout(() => {
        resolve("成功");
    }, 1000);
});

promise.then(value => {
    console.log(value); // 成功
});

文章作者: 弈心
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 弈心 !
评论
 上一篇
前端面试准备指南六之前端工程化篇 前端面试准备指南六之前端工程化篇
前端工程化核心知识点,涵盖模块化规范、Webpack构建工具、构建优化及工程化最佳实践,包括CommonJS/AMD/CMD/ES Module、Webpack配置/Loader/Plugin、代码分割、Tree Shaking、CI/CD等
下一篇 
浅谈JavaScript设计模式之发布订阅者模式 浅谈JavaScript设计模式之发布订阅者模式
深入解析JavaScript中的发布订阅者模式,包括概念、实现、应用场景和最佳实践
2026-05-03
  目录