前端面试题之JS相关

358 阅读15分钟

1. 对原生 JavaScript 了解程度

面试题:请简要阐述你对原生 JavaScript 的理解,以及它在前端开发中的重要性。
答案解析:原生 JavaScript 指的是不依赖任何第三方库和框架,直接使用 JavaScript 语言本身的特性进行编程。它是前端开发的基础,用于实现网页的交互效果、数据处理、与服务器通信等功能。在前端开发中,原生 JavaScript 非常重要,因为它是所有前端框架和库的基础,理解原生 JavaScript 有助于更好地理解和使用这些工具,同时也能在不适合使用框架的场景下直接进行开发,提高代码的性能和可维护性。

2. 闭包

面试题:详细解释闭包的概念,并给出一个闭包在实际开发中用于数据封装的示例。
答案解析

  • 概念:闭包是指有权访问另一个函数作用域中的变量的函数。即使该函数已经执行完毕,其作用域内的变量也不会被销毁,因为闭包会持有对这些变量的引用。
  • 示例

javascript

function createUser() {
    let privateData = {
        username: 'JohnDoe',
        password: 'secret123'
    };
    return {
        getUsername: function() {
            return privateData.username;
        },
        setUsername: function(newUsername) {
            privateData.username = newUsername;
        }
    };
}
const user = createUser();
console.log(user.getUsername()); // 输出: JohnDoe
user.setUsername('JaneDoe');
console.log(user.getUsername()); // 输出: JaneDoe

在这个示例中,getUsername 和 setUsername 函数形成了闭包,它们可以访问 createUser 函数内部的 privateData 对象,而外部无法直接访问该对象,实现了数据的封装。

3. 作用域链

面试题:请解释作用域链的工作原理,以及 JavaScript 引擎如何通过作用域链查找变量。
答案解析:作用域链是 JavaScript 中一个非常重要的概念,它决定了变量和函数的作用范围,以及 JavaScript 引擎如何查找变量。下面详细解释作用域链的工作原理以及 JavaScript 引擎如何通过作用域链查找变量。

作用域链的工作原理
作用域的概念

作用域定义了变量和函数的可访问范围。在 JavaScript 中有两种主要的作用域:全局作用域和函数作用域(ES6 引入了块级作用域)。

  • 全局作用域:在代码的最外层定义的变量和函数属于全局作用域,它们可以在代码的任何地方被访问。
  • 函数作用域:在函数内部定义的变量和函数只能在该函数内部被访问,外部无法直接访问。
  • 块级作用域:使用 let 和 const 关键字在 if 语句、for 循环等代码块中定义的变量,只能在该代码块内部被访问。
作用域链的形成

当一个函数被创建时,它会保存一个对其外部作用域的引用。这个引用形成了一个链表结构,称为作用域链。作用域链的每一个节点都是一个变量对象,变量对象中存储了该作用域内的变量和函数。

当一个函数被调用时,会创建一个执行上下文,执行上下文包含三个部分:变量对象、作用域链和 this 指针。作用域链的头部是当前执行上下文的变量对象,后面依次连接着外部作用域的变量对象,直到全局作用域的变量对象。

JavaScript 引擎如何通过作用域链查找变量

当 JavaScript 引擎需要查找一个变量时,它会按照以下步骤进行:

  1. 从当前执行上下文的变量对象开始查找:首先,引擎会在当前执行上下文的变量对象中查找该变量。如果找到了,就返回该变量的值。
  2. 沿着作用域链向上查找:如果在当前执行上下文的变量对象中没有找到该变量,引擎会沿着作用域链向上查找,依次检查外部作用域的变量对象。直到找到该变量或者到达作用域链的末尾(全局作用域)。
  3. 变量未找到:如果在整个作用域链中都没有找到该变量,引擎会抛出 ReferenceError 错误。
示例代码
// 全局作用域 
var globalVariable = 'I am a global variable'; 
function outerFunction() { 
    // 外部函数作用域 
    var outerVariable = 'I am an outer variable'; 
    function innerFunction() { 
        // 内部函数作用域 
        var innerVariable = 'I am an inner variable'; 
        // 查找变量 
        console.log(innerVariable); // 直接在当前作用域找到变量 
        console.log(outerVariable); // 当前作用域未找到,沿着作用域链在外部作用域找到变量 
        console.log(globalVariable); // 当前作用域和外部作用域都未找到,在全局作用域找到变量 
        // 尝试访问不存在的变量 
        // console.log(nonExistentVariable); // 抛出 ReferenceError 错误 
    } 
    innerFunction(); 
   } 
   outerFunction();

在上述代码中,innerFunction 内部查找变量的过程如下:

  • 当查找 innerVariable 时,引擎直接在 innerFunction 的变量对象中找到了该变量,返回其值。
  • 当查找 outerVariable 时,引擎在 innerFunction 的变量对象中没有找到该变量,于是沿着作用域链向上查找,在 outerFunction 的变量对象中找到了该变量,返回其值。
  • 当查找 globalVariable 时,引擎在 innerFunction 和 outerFunction 的变量对象中都没有找到该变量,继续沿着作用域链向上查找,在全局作用域的变量对象中找到了该变量,返回其值。

通过作用域链,JavaScript 引擎可以确保变量的查找遵循一定的规则,保证了变量的访问和使用的正确性。

4. 面向对象(封装,继承,多态)

面试题:分别解释面向对象编程中的封装、继承和多态,并给出一个简单的 JavaScript 示例说明多态。
答案解析

  • 封装:封装是将数据和操作数据的方法捆绑在一起,并隐藏对象的内部实现细节,只对外提供公共的访问接口。这样可以保护数据不被外部随意修改,提高代码的安全性和可维护性。
  • 继承:继承是指一个对象直接使用另一对象的属性和方法。通过继承,可以实现代码的复用,减少重复代码。
  • 多态:多态是指不同对象对同一消息做出不同的响应。在 JavaScript 中,多态通常通过方法重写来实现。 多态示例
class Animal {
    speak() {
        return 'Animal makes a sound';
    }
}
class Dog extends Animal {
    speak() {
        return 'Woof!';
    }
}
class Cat extends Animal {
    speak() {
        return 'Meow!';
    }
}
function makeAnimalSpeak(animal) {
    console.log(animal.speak());
}
const dog = new Dog();
const cat = new Cat();
makeAnimalSpeak(dog); // 输出: Woof!
makeAnimalSpeak(cat); // 输出: Meow!

在这个示例中,Dog 和 Cat 类都继承自 Animal 类,并重写了 speak 方法。makeAnimalSpeak 函数可以接受不同类型的 Animal 对象,并调用它们的 speak 方法,实现了多态。

5. JavaScript 如何实现继承

面试题:请列举三种 JavaScript 实现继承的方式,并详细说明其中一种的实现原理。
答案解析

  • 原型链继承:将子类的原型设置为父类的实例。
  • 构造函数继承:在子类构造函数中调用父类构造函数。
  • 组合继承:结合了原型链继承和构造函数继承的优点。 以组合继承为例说明实现原理
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;
}
Child.prototype = Object.create(Parent.prototype); // 原型链继承,继承父类的方法
Child.prototype.constructor = Child;
const child = new Child('John', 10);
child.sayName(); // 输出: John

组合继承的原理是通过 Parent.call(this, name) 在子类构造函数中调用父类构造函数,为子类实例初始化父类的属性,避免了原型链继承中父类属性被所有子类实例共享的问题。同时,通过 Object.create(Parent.prototype) 将子类的原型设置为父类原型的一个副本,使得子类实例可以继承父类原型上的方法。

6. JavaScript 原型,原型链

面试题:解释 JavaScript 中原型和原型链的概念,并说明原型链的查找机制。
答案解析

  • 原型:在 JavaScript 中,每个对象都有一个内部属性 [[Prototype]](在浏览器中可以通过 __proto__ 访问),它指向该对象的原型对象。原型对象也是一个普通对象,它也有自己的原型,以此类推,直到最顶层的 Object.prototype,其 [[Prototype]] 为 null
  • 原型链:由对象的 [[Prototype]] 连接而成的链条就是原型链。
  • 查找机制:当访问一个对象的属性或方法时,JavaScript 引擎首先在该对象本身查找。如果找不到,就会沿着原型链向上查找,依次检查每个原型对象,直到找到该属性或方法或者到达原型链的顶端(Object.prototype)。如果在 Object.prototype 中也找不到,就会返回 undefined
const obj = {};
obj.__proto__.message = 'Hello from prototype';
console.log(obj.message); // 输出: Hello from prototype

在这个示例中,obj 对象本身没有 message 属性,JavaScript 引擎会沿着原型链在 obj 的原型对象中找到该属性。

7. 面向对象编程及面向过程编程

面试题:简述面向对象编程和面向过程编程的概念,比较它们的异同和优缺点。
答案解析

  • 面向对象编程(OOP) :是一种以对象为中心的编程范式,将数据和操作数据的方法封装在对象中,通过对象之间的交互来实现功能。OOP 的主要特性包括封装、继承和多态。

  • 面向过程编程(POP) :是一种以过程为中心的编程范式,将问题分解为一系列的步骤,通过函数的调用和数据的传递来实现功能。

  • 相同点:都是编程范式,目的都是解决问题和实现软件功能。

  • 不同点

    • 编程思想:OOP 强调对象的概念,将数据和行为封装在一起;POP 强调过程的概念,将数据和操作分离。
    • 代码组织:OOP 通过类和对象来组织代码,具有更好的模块化和可维护性;POP 通过函数来组织代码,代码结构相对简单。
  • 优点

    • OOP:代码可维护性、可扩展性和可复用性强,适合开发大型复杂的软件系统。
    • POP:代码逻辑清晰,易于理解和实现,适合处理简单的问题。
  • 缺点

    • OOP:代码复杂度较高,需要一定的设计和抽象能力,性能开销相对较大。
    • POP:代码复用性差,难以应对复杂的软件系统,代码的可维护性和可扩展性较差。

8. 事件模型(事件代理、捕获事件、冒泡事件)

面试题:解释什么是事件代理(事件委托),并说明它与捕获事件和冒泡事件的关系。
答案解析

  • 事件代理(事件委托) :是指将事件处理程序绑定到一个父元素上,而不是绑定到每个子元素上。当子元素上的事件触发时,事件会冒泡到父元素上,父元素上的事件处理程序会根据事件的目标元素来处理该事件。这样可以减少事件处理程序的数量,提高性能,同时也方便动态添加和删除子元素。

  • 捕获事件和冒泡事件

    • 捕获事件:事件从最外层的元素开始,依次向内层元素传播,直到到达目标元素。捕获阶段是事件传播的第一个阶段。
    • 冒泡事件:事件从目标元素开始,依次向外层元素传播,直到到达最外层的元素。冒泡阶段是事件传播的第二个阶段。
  • 关系:事件代理利用了事件冒泡的机制。当子元素触发事件后,事件会冒泡到父元素,父元素上的事件处理程序就可以捕获到该事件并进行处理。因此,事件代理是基于事件冒泡实现的一种优化技术。

html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
</head>
<body>
    <ul id="list">
        <li>Item 1</li>
        <li>Item 2</li>
        <li>Item 3</li>
    </ul>
    <script>
        const list = document.getElementById('list');
        list.addEventListener('click', function(event) {
            if (event.target.tagName === 'LI') {
                console.log('Clicked on: ', event.target.textContent);
            }
        });
    </script>
</body>
</html>

在这个示例中,将点击事件处理程序绑定到 ul 元素上,当点击 li 元素时,事件会冒泡到 ul 元素,ul 元素上的事件处理程序会根据 event.target 来判断是哪个 li 元素被点击了。

9. 浏览器的事件循环(宏任务,微任务)

面试题:请简述浏览器的事件循环机制,包括宏任务和微任务的执行顺序。
答案解析:浏览器的事件循环是 JavaScript 实现异步编程的核心机制,它负责处理异步任务的回调函数。

  • 宏任务:包括 setTimeoutsetIntervalsetImmediate(Node.js 环境)、I/O 操作、UI 渲染等。宏任务会被放入宏任务队列中。
  • 微任务:包括 Promise.thenMutationObserverprocess.nextTick(Node.js 环境)等。微任务会被放入微任务队列中。

执行顺序

  1. 从宏任务队列中取出一个宏任务执行。
  2. 执行完一个宏任务后,检查微任务队列,如果微任务队列中有任务,则依次执行微任务队列中的所有任务,直到微任务队列为空。
  3. 重复步骤 1 和 2,不断循环执行。

javascript

console.log('Start');
setTimeout(() => {
    console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
    console.log('Promise then');
});
console.log('End');

执行结果:

plaintext

Start
End
Promise then
setTimeout

解释:首先执行同步代码,输出 Start 和 End。然后 Promise.resolve().then 产生一个微任务,放入微任务队列。setTimeout 产生一个宏任务,放入宏任务队列。同步代码执行完后,检查微任务队列,执行微任务,输出 Promise then。接着从宏任务队列中取出任务执行,输出 setTimeout

10. Js 的基本数据类型和引用数据类型(堆,栈)

面试题:请列举 JavaScript 的基本数据类型和引用数据类型,并说明它们在内存中的存储方式。
答案解析

  • 基本数据类型:包括 numberstringbooleannullundefinedsymbol 和 bigint。基本数据类型的值直接存储在栈内存中,它们的访问速度较快。
  • 引用数据类型:包括 object(如 ArrayFunctionDate 等)。引用数据类型的值存储在堆内存中,而在栈内存中存储的是该对象在堆内存中的引用地址。通过引用地址可以访问堆内存中的对象。
let num = 10; // 基本数据类型,存储在栈内存
let obj = { name: 'John' }; // 引用数据类型,name 存储在堆内存,obj 存储引用地址在栈内存

11. 防抖和节流

面试题:解释防抖和节流的概念,并分别给出一个简单的实现示例。
答案解析

  • 防抖:防抖是指在一定时间内,只有最后一次触发事件才会执行相应的函数。常用于搜索框输入提示、窗口大小改变等场景。
function debounce(func, delay) {
    let timer = null;
    return function() {
        const context = this;
        const args = arguments;
        clearTimeout(timer);
        timer = setTimeout(() => {
            func.apply(context, args);
        }, delay);
    };
}
function search() {
    console.log('Searching...');
}
const debouncedSearch = debounce(search, 300);
window.addEventListener('input', debouncedSearch);
  • 节流:节流是指在一定时间内,只执行一次函数。常用于滚动加载、按钮点击等场景。
function throttle(func, delay) {
    let timer = null;
    return function() {
        if (!timer) {
            const context = this;
            const args = arguments;
            func.apply(context, args);
            timer = setTimeout(() => {
                timer = null;
            }, delay);
        }
    };
}
function loadMore() {
    console.log('Loading more...');
}
const throttledLoadMore = throttle(loadMore, 500);
window.addEventListener('scroll', throttledLoadMore);

12. 强缓存和协商缓存

面试题:请解释强缓存和协商缓存的概念,以及它们分别是如何控制的。
答案解析

  • 强缓存:浏览器直接从本地缓存中读取资源,不需要向服务器发送请求。强缓存通过 Expires 和 Cache-Control 两个 HTTP 头来控制。

    • Expires:是 HTTP 1.0 中的字段,指定资源的过期时间。例如:Expires: Thu, 31 Dec 2025 23:59:59 GMT
    • Cache-Control:是 HTTP 1.1 中的字段,优先级高于 Expires,可以设置资源的缓存时间、是否允许缓存等。例如:Cache-Control: max-age=3600 表示资源在 1 小时内有效。
  • 协商缓存:浏览器在使用本地缓存之前,会先向服务器发送一个请求,询问服务器该资源是否有更新。如果服务器返回 304 状态码,则表示资源没有更新,浏览器可以使用本地缓存;如果服务器返回新的资源,则浏览器会使用新的资源。协商缓存通过 Last-Modified 和 ETag 两个 HTTP 头来控制。

    • Last-Modified:表示资源的最后修改时间。例如:Last-Modified: Tue, 25 Feb 2025 12:00:00 GMT
    • ETag:是资源的唯一标识符。例如:ETag: "abc123"
      浏览器在发送请求时,会携带 If-Modified-Since 或 If-None-Match 头,分别对应服务器返回的 Last-Modified 和 ETag。服务器根据这些信息判断资源是否有更新。

13. 谈谈 This 对象的理解,this 的指向

面试题:详细阐述 this 对象在不同场景下的指向,并举例说明。
答案解析

  • 全局作用域:在全局作用域中,this 指向全局对象。在浏览器环境里是 window 对象。
console.log(this === window); // true
  • 函数内部(非严格模式) :作为普通函数调用时,this 指向全局对象。
function test() {
    console.log(this === window); // true
}
test();
  • 函数内部(严格模式) :严格模式下,普通函数调用时 this 为 undefined
function strictTest() {
    'use strict';
    console.log(this === undefined); // true
}
strictTest();
  • 对象方法:当函数作为对象的方法调用时,this 指向调用该方法的对象。
const obj = {
    name: 'John',
    sayName() {
        console.log(this.name);
    }
};
obj.sayName(); // John
  • 构造函数:使用 new 调用构造函数时,this 指向新创建的对象。
function Person(name) {
    this.name = name;
}
const person = new Person('Alice');
console.log(person.name); // Alice
  • callapplybind 方法:可以显式地指定 this 的指向。
function greet() {
    console.log(`Hello, ${this.name}`);
}
const person1 = { name: 'Bob' };
greet.call(person1); // Hello, Bob

14. new 操作符具体干了什么呢?

面试题:简述 new 操作符在创建对象时的具体步骤。
答案解析
当使用 new 操作符调用一个构造函数时,会按以下步骤执行:

  1. 创建新对象:创建一个全新的空对象。
  2. 设置原型:将新对象的 [[Prototype]] 属性设置为构造函数的 prototype 属性,使新对象能够继承构造函数原型上的属性和方法。
  3. 执行构造函数:以新对象作为 this 的值执行构造函数,并将传入 new 操作符的参数传递给构造函数。
  4. 返回对象:如果构造函数返回一个对象,则返回该对象;否则返回新创建的对象。
    示例代码如下:
function myNew(constructor, ...args) {
    const obj = {};
    obj.__proto__ = constructor.prototype;
    const result = constructor.apply(obj, args);
    return typeof result === 'object' && result!== null? result : obj;
}
function Person(name) {
    this.name = name;
}
const person = myNew(Person, 'Eve');
console.log(person.name); // Eve

15. 如何解决跨域问题?

面试题:列举至少三种常见的跨域解决方案,并简要说明其原理。
答案解析

  • JSONP(JSON with Padding) :利用 <script> 标签的 src 属性不受同源策略限制的特点。前端创建一个回调函数,然后将回调函数名作为参数添加到请求的 URL 中,服务器收到请求后,将数据包装在回调函数中返回,前端的 <script> 标签会执行这个回调函数,从而获取到数据。

html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
</head>
<body>
    <script>
        function handleData(data) {
            console.log(data);
        }
        const script = document.createElement('script');
        script.src = 'http://example.com/api?callback=handleData';
        document.body.appendChild(script);
    </script>
</body>
</html>
  • CORS(跨域资源共享) :服务器端设置响应头,允许特定的跨域请求。当浏览器检测到请求是跨域请求时,会在请求头中添加 Origin 字段,服务器根据这个字段判断是否允许该请求,如果允许,则在响应头中添加 Access - Control - Allow - Origin 等相关字段,浏览器收到响应后,会根据这些响应头判断是否允许访问该资源。

javascript

// Node.js 示例
const express = require('express');
const app = express();
app.use((req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
    next();
});
  • 代理服务器:在同源的服务器上设置一个代理,前端将请求发送到同源的代理服务器,代理服务器再将请求转发到目标服务器,并将目标服务器的响应返回给前端。这样对于前端来说,请求是同源的,不会受到跨域限制。

javascript

// Vue.js 项目中使用 vue.config.js 配置代理
module.exports = {
    devServer: {
        proxy: {
            '/api': {
                target: 'http://example.com',
                changeOrigin: true,
                pathRewrite: {
                    '^/api': ''
                }
            }
        }
    }
};

16. 谈谈你对 webpack 的看法

面试题:阐述你对 Webpack 的理解,包括它的主要功能和优势。
答案解析
Webpack 是一个现代 JavaScript 应用程序的静态模块打包工具。

  • 主要功能

    • 模块打包:可以将各种类型的模块(如 JavaScript、CSS、图片等)打包成一个或多个文件,减少浏览器的请求次数,提高页面加载速度。
    • 资源处理:通过加载器(loader)可以对不同类型的文件进行处理,例如将 ES6+ 代码转换为兼容旧浏览器的代码,将 Sass 或 Less 转换为 CSS 等。
    • 代码分割:支持将代码分割成多个小块,实现按需加载,提高应用的性能。
    • 插件系统:拥有丰富的插件生态系统,可以实现各种功能,如代码压缩、生成 HTML 文件、热更新等。
  • 优势

    • 提高性能:通过打包和代码分割,减少了浏览器的请求次数,优化了资源加载,提高了页面的响应速度。
    • 模块化开发:支持各种模块规范,方便开发者进行模块化开发,提高代码的可维护性和复用性。
    • 高度可定制:可以根据项目的需求配置不同的加载器和插件,实现个性化的打包流程。

17. 常见 web 安全及防护原理

面试题:列举三种常见的 Web 安全问题,并说明相应的防护原理。
答案解析

  • XSS(跨站脚本攻击) :攻击者通过在网页中注入恶意脚本,当用户访问该页面时,脚本会在用户的浏览器中执行,从而获取用户的敏感信息。防护原理主要是对用户输入进行过滤和转义,将特殊字符转换为 HTML 实体,防止恶意脚本的注入。同时,可以使用 Content Security Policy(CSP)来限制页面可以加载的资源来源,减少 XSS 攻击的风险。

javascript

// 对用户输入进行转义
function escapeHTML(str) {
    return str.replace(/[&<>"']/g, function (match) {
        switch (match) {
            case '&': return '&amp;';
            case '<': return '&lt;';
            case '>': return '&gt;';
            case '"': return '&quot;';
            case "'": return '&#039;';
        }
    });
}
  • CSRF(跨站请求伪造) :攻击者诱导用户在已登录的网站上执行恶意操作,利用用户的身份信息进行非法请求。防护原理主要是使用 CSRF 令牌,在表单或请求中添加一个随机生成的令牌,服务器在处理请求时会验证该令牌的有效性。另外,设置 SameSite 属性可以限制 Cookie 只能在同站请求中发送,减少 CSRF 攻击的可能性。

html

<form action="/submit" method="post">
    <input type="hidden" name="csrf_token" value="123456789">
    <input type="text" name="message">
    <input type="submit" value="Submit">
</form>
  • SQL 注入:攻击者通过在输入框中输入恶意的 SQL 语句,来篡改或获取数据库中的数据。防护原理主要是对用户输入进行严格的验证和过滤,使用参数化查询,避免直接将用户输入拼接到 SQL 语句中。

javascript

// 使用参数化查询(以 Node.js 和 MySQL 为例)
const mysql = require('mysql');
const connection = mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'password',
    database: 'test'
});
const username = 'test';
const query = 'SELECT * FROM users WHERE username =?';
connection.query(query, [username], function (error, results) {
    if (error) throw error;
    console.log(results);
});

18. 用过哪些设计模式?

面试题:请列举至少三种你熟悉的设计模式,并简要说明其应用场景。
答案解析

  • 单例模式:确保一个类只有一个实例,并提供一个全局访问点。应用场景包括配置文件管理、数据库连接池等,避免多次创建相同的对象,节省系统资源。

javascript

class Singleton {
    constructor() {
        if (!Singleton.instance) {
            this.data = [];
            Singleton.instance = this;
        }
        return Singleton.instance;
    }
    addItem(item) {
        this.data.push(item);
    }
    getItems() {
        return this.data;
    }
}
const singleton1 = new Singleton();
const singleton2 = new Singleton();
console.log(singleton1 === singleton2); // true
  • 观察者模式:定义了对象之间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会得到通知并自动更新。应用场景包括事件处理、状态管理等。

javascript

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);
        }
    }
}
const emitter = new EventEmitter();
const callback = () => console.log('Event fired');
emitter.on('testEvent', callback);
emitter.emit('testEvent');
  • 工厂模式:定义一个创建对象的接口,让子类决定实例化哪个类。应用场景包括创建不同类型的对象,根据不同的条件返回不同的实例。

javascript

class ShapeFactory {
    createShape(type) {
        if (type === 'circle') {
            return new Circle();
        } else if (type === 'square') {
            return new Square();
        }
        return null;
    }
}
class Circle {
    draw() {
        console.log('Drawing a circle');
    }
}
class Square {
    draw() {
        console.log('Drawing a square');
    }
}
const factory = new ShapeFactory();
const circle = factory.createShape('circle');
circle.draw();

19. primise、promise 设计模式和 promise A + 规范

面试题:解释 Promise 的概念、Promise 设计模式的作用以及 Promise A+ 规范的主要内容。
答案解析

  • Promise 概念Promise 是一种异步编程的解决方案,用于处理异步操作的结果。它有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。一旦状态确定,就不会再改变。

javascript

const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('Success');
    }, 1000);
});
promise.then(result => {
    console.log(result);
}).catch(error => {
    console.error(error);
});
  • Promise 设计模式的作用:解决了异步编程中的回调地狱问题,使异步代码的结构更加清晰和易于维护。通过 then 方法可以链式调用,实现异步操作的顺序执行。

  • Promise A+ 规范的主要内容

    • 状态管理Promise 必须有三种状态,状态的转变只能从 pending 到 fulfilled 或从 pending 到 rejected,且状态一旦改变就不能再变。
    • then 方法Promise 必须提供一个 then 方法,用于处理 Promise 的结果。then 方法接受两个可选参数,分别是成功回调和失败回调,并且可以链式调用。
    • 链式调用then 方法必须返回一个新的 Promise,以便实现链式调用。

20. 谈谈你对 AMD、CMD 的理解 Commonjs esmodule 标准

面试题:简述 AMD、CMD、CommonJS 和 ESModule 的概念和特点。
答案解析

  • AMD(Asynchronous Module Definition) :异步模块定义规范,主要用于浏览器环境。其特点是异步加载模块,通过 define 函数来定义模块,require 函数来加载模块。模块的依赖关系在定义时就确定了,适合在网络环境中异步加载模块。

javascript

// 定义模块
define(['dependency1', 'dependency2'], function (dep1, dep2) {
    return {
        init: function () {
            // 使用 dep1 和 dep2
        }
    };
});
// 加载模块
require(['module'], function (module) {
    module.init();
});
  • CMD(Common Module Definition) :通用模块定义规范,也是用于浏览器环境。它采用就近依赖的原则,在模块内部需要使用某个依赖时再进行加载。模块的定义使用 define 函数。

javascript

define(function (require, exports, module) {
    const dep1 = require('dependency1');
    const dep2 = require('dependency2');
    exports.init = function () {
        // 使用 dep1 和 dep2
    };
});
  • CommonJS:是服务器端模块规范,主要用于 Node.js 环境。使用 require 函数来引入模块,exports 或 module.exports 来导出模块。模块是同步加载的,适合在服务器端环境中使用。

javascript

// 导出模块
const add = (a, b) => a + b;
module.exports = {
    add
};
// 引入模块
const math = require('./math');
console.log(math.add(1, 2));
  • ESModule:是 ES6 引入的官方模块规范,支持静态导入和导出。在浏览器和 Node.js 中都有一定的支持。使用 import 语句导入模块,export 语句导出模块。

javascript

// 导出模块
export const add = (a, b) => a + b;
// 导入模块
import { add } from './math.js';
console.log(add(1, 2));

21. 同步和异步的区别

面试题:阐述同步和异步的概念,并说明它们的区别。
答案解析

  • 同步:同步操作是指代码按照顺序依次执行,当前操作执行完毕后才会执行下一个操作。在同步操作中,如果某个操作需要较长时间才能完成,会阻塞后续代码的执行。例如:

javascript

console.log('Start');
const result = 1 + 2;
console.log(result);
console.log('End');

在这个例子中,代码会依次执行,先输出 Start,然后计算 1 + 2 的结果并输出,最后输出 End

  • 异步:异步操作是指代码在执行过程中不会阻塞后续代码的执行,当异步操作开始后,会继续执行后续代码,等异步操作完成后,通过回调函数、Promise 或 async/await 等方式通知程序处理结果。例如:

javascript

console.log('Start');
setTimeout(() => {
    console.log('Async operation completed');
}, 1000);
console.log('End');

在这个例子中,setTimeout 是一个异步操作,代码不会等待 setTimeout 的回调函数执行完毕,而是会继续执行后续代码,所以会先输出 Start 和 End,1 秒后再输出 Async operation completed

22. 说说严格模式的限制

面试题:列举至少三种严格模式的限制。
答案解析: 严格模式(Strict Mode)是 ECMAScript 5 引入的一种模式,它对 JavaScript 的语法和行为施加了更严格的规则,有助于编写更安全、更规范的代码。以下是严格模式的一些主要限制:

1. 变量声明方面

  • 禁止未声明变量:在严格模式下,使用未声明的变量会抛出 ReferenceError 错误。在正常模式中,给未声明的变量赋值会隐式地创建一个全局变量,这可能导致意外的全局变量污染。

javascript

// 严格模式
'use strict';
x = 10; // 报错:ReferenceError: x is not defined

// 正常模式
y = 20; // 会创建一个全局变量 y
  • 禁止删除不可删除的属性:在严格模式下,尝试删除不可配置(configurable 为 false)的属性会抛出 TypeError 错误。而在正常模式下,这种删除操作会默默失败。

javascript

// 严格模式
'use strict';
const obj = {};
Object.defineProperty(obj, 'prop', {
    value: 1,
    configurable: false
});
delete obj.prop; // 报错:TypeError: Cannot delete property 'prop' of #<Object>

// 正常模式
const normalObj = {};
Object.defineProperty(normalObj, 'prop', {
    value: 1,
    configurable: false
});
delete normalObj.prop; // 不会报错,但删除失败

2. 函数和参数方面

  • 函数参数名不能重复:在严格模式下,函数的参数名不能重复。而在正常模式中,后面的参数会覆盖前面同名的参数。

javascript

// 严格模式
'use strict';
function func(a, a) { // 报错:SyntaxError: Duplicate parameter name not allowed in this context
    return a;
}

// 正常模式
function normalFunc(a, a) {
    return a;
}
  • 禁止使用 with 语句with 语句会改变变量的作用域链,可能导致代码的可读性和可维护性变差,还容易引发变量名冲突。在严格模式下,使用 with 语句会抛出 SyntaxError 错误。

javascript

// 严格模式
'use strict';
const obj = { x: 1 };
with (obj) {
    console.log(x); // 报错:SyntaxError: Strict mode code may not include a with statement
}

// 正常模式
const normalObj = { x: 1 };
with (normalObj) {
    console.log(x); // 输出 1
}

3. this 指向方面

  • 函数内 this 不会指向全局对象:在严格模式下,函数内部的 this 如果没有显式绑定,其值为 undefined,而不是全局对象(在浏览器中是 window)。这有助于避免意外地修改全局对象。

javascript

// 严格模式
'use strict';
function test() {
    console.log(this); // 输出:undefined
}
test();

// 正常模式
function normalTest() {
    console.log(this); // 输出:window 对象
}
normalTest();
  • 构造函数未使用 new 调用时 this 为 undefined:在严格模式下,如果构造函数没有使用 new 关键字调用,this 会是 undefined,这可以避免创建意外的全局对象。

javascript

// 严格模式
'use strict';
function Person(name) {
    this.name = name; // 如果没有使用 new 调用,这里会报错:TypeError: Cannot set property 'name' of undefined
}
const p = Person('John'); 

// 正常模式
function NormalPerson(name) {
    this.name = name; // 会创建一个全局变量 name
}
const np = NormalPerson('Jane');

4. 其他方面

  • 禁止八进制字面量:在严格模式下,不允许使用以 0 开头的八进制字面量(ES6 之前的八进制表示方式)。而在正常模式中,这种表示方式是允许的。

javascript

// 严格模式
'use strict';
const num = 010; // 报错:SyntaxError: Octal literals are not allowed in strict mode
  • 禁止使用 eval 创建变量:在严格模式下,eval 执行的代码不会在调用它的作用域中创建变量。eval 执行的代码有自己独立的作用域,这可以避免一些潜在的安全问题。

javascript

// 严格模式
'use strict';
eval('var x = 10');
console.log(typeof x); // 输出:undefined

// 正常模式
eval('var y = 20');
console.log(typeof y); // 输出:number

23. 谈谈你对 ES6 的理解

面试题:请阐述你对 ES6 的理解,包括它带来的主要特性和对 JavaScript 开发的影响。
答案解析
ES6 即 ECMAScript 6.0,是 JavaScript 语言的下一代标准,带来了许多新特性和语法糖,提升了代码的可读性、可维护性和开发效率。

  • 主要特性

    • 块级作用域:引入 let 和 const 关键字,解决了 var 存在的变量提升和作用域问题。
    • 箭头函数:简化了函数的定义,并且没有自己的 thisargumentssuper 或 new.target,它的 this 值继承自外层函数。
    • 类和继承:引入了 class 关键字和 extends 关键字,使 JavaScript 具备了更直观的面向对象编程能力。
    • Promise 对象:用于处理异步操作,避免回调地狱,使异步代码更易于管理。
    • 模块化:通过 import 和 export 关键字实现了静态模块导入和导出,方便代码的组织和复用。
    • 解构赋值:可以方便地从数组或对象中提取值并赋值给变量。
  • 对 JavaScript 开发的影响

    • 提高开发效率:新特性减少了样板代码,使代码更加简洁,开发者可以更快地实现功能。
    • 增强代码可读性:更直观的语法和结构让代码更易于理解和维护。
    • 推动前端框架发展:许多现代前端框架(如 React、Vue.js)都大量使用了 ES6 的特性。

24. async/await 实现原理

面试题:解释 async/await 的实现原理,并给出一个简单的示例。
答案解析
async/await 是基于 Promise 实现的异步编程语法糖,目的是让异步代码看起来更像同步代码。

  • 实现原理

    • async 函数会返回一个 Promise 对象,函数内部的 await 关键字只能用于等待一个 Promise 对象。
    • 当 async 函数执行时,遇到 await 表达式,会暂停函数的执行,等待 Promise 解决(fulfilled)或拒绝(rejected)。
    • 当 Promise 解决后,await 表达式会返回 Promise 的解决值,然后继续执行 async 函数后续的代码。
  • 示例

javascript

function fetchData() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Data fetched');
        }, 1000);
    });
}

async function getData() {
    try {
        const data = await fetchData();
        console.log(data);
    } catch (error) {
        console.error(error);
    }
}

getData();

25. generator 生成器函数

面试题:介绍 generator 生成器函数的概念、特点和使用场景。
答案解析

  • 概念:生成器函数是一种特殊的函数,使用 function* 定义。它可以暂停和恢复执行,每次调用 next() 方法时,函数会执行到下一个 yield 表达式处暂停。

  • 特点

    • 可以通过 yield 关键字返回多个值,而普通函数只能返回一个值。
    • 调用生成器函数不会立即执行函数体,而是返回一个迭代器对象。
    • 可以通过 next() 方法控制函数的执行流程。
  • 使用场景

    • 实现迭代器:可以方便地创建自定义的迭代器。
    • 异步编程:结合 Promise 可以实现异步操作的顺序执行。
  • 示例

javascript

function* generator() {
    yield 1;
    yield 2;
    yield 3;
}

const gen = generator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }

26. interator 迭代器和 for of 循环

面试题:解释迭代器的概念,以及 for of 循环与迭代器的关系。
答案解析

  • 迭代器概念:迭代器是一个对象,它实现了 next() 方法,每次调用 next() 方法会返回一个包含 value 和 done 属性的对象。value 表示当前迭代的值,done 是一个布尔值,表示迭代是否结束。
  • for of 循环与迭代器的关系for of 循环是一种用于遍历可迭代对象的语法糖。可迭代对象是指实现了 Symbol.iterator 方法的对象,该方法返回一个迭代器对象。当使用 for of 循环遍历可迭代对象时,会自动调用该对象的 Symbol.iterator 方法获取迭代器,然后不断调用迭代器的 next() 方法,直到 done 属性为 true
  • 示例

收起

javascript

const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

// 使用 for of 循环遍历数组
for (const num of arr) {
    console.log(num);
}

27. 谈一谈你理解的函数式编程

面试题:阐述函数式编程的概念、特点和优势。
答案解析

  • 概念:函数式编程是一种编程范式,它将计算视为函数的求值,避免使用共享状态和可变数据。函数式编程强调函数的纯粹性,即一个函数的输出只取决于输入,不产生任何副作用。

  • 特点

    • 纯函数:相同的输入始终返回相同的输出,不会修改外部状态。
    • 不可变数据:数据一旦创建就不能被修改,如果需要修改数据,会返回一个新的数据副本。
    • 高阶函数:函数可以作为参数传递给其他函数,也可以作为返回值返回。
    • 函数组合:将多个简单的函数组合成一个复杂的函数。
  • 优势

    • 可维护性:纯函数的输出只依赖于输入,使得代码更易于理解和调试。
    • 可测试性:由于纯函数没有副作用,测试更加简单。
    • 并行处理:不可变数据和纯函数使得代码更容易进行并行处理。

28. 谈一谈箭头函数与普通函数的区别?

面试题:请列举箭头函数与普通函数的至少三个区别。
答案解析

  • 语法:箭头函数使用更简洁的语法,省略了 function 关键字,对于单个参数可以省略括号,对于单个表达式可以省略花括号和 return 关键字。

javascript

// 普通函数
function add(a, b) {
    return a + b;
}

// 箭头函数
const addArrow = (a, b) => a + b;
  • this 指向:普通函数的 this 指向取决于函数的调用方式,而箭头函数没有自己的 this,它的 this 值继承自外层函数。

javascript

const obj = {
    name: 'John',
    sayName: function () {
        console.log(this.name);
    },
    sayNameArrow: () => {
        console.log(this.name);
    }
};

obj.sayName(); // John
obj.sayNameArrow(); // undefined
  • arguments 对象:普通函数有自己的 arguments 对象,包含了函数调用时的所有参数。而箭头函数没有自己的 arguments 对象,它会继承外层函数的 arguments 对象。
  • 构造函数:普通函数可以使用 new 关键字作为构造函数创建对象,而箭头函数不能使用 new 关键字,否则会抛出错误。

29. 异步编程的实现方式

面试题:列举至少三种异步编程的实现方式,并分别给出示例。
答案解析

  • 回调函数:将一个函数作为参数传递给另一个异步函数,当异步操作完成时调用该回调函数。

javascript

function fetchData(callback) {
    setTimeout(() => {
        const data = 'Data fetched';
        callback(data);
    }, 1000);
}

fetchData((data) => {
    console.log(data);
});
  • Promise:使用 Promise 对象来处理异步操作,通过 then 方法处理成功结果,通过 catch 方法处理错误。

javascript

function fetchData() {
    return new Promise((resolve) => {
        setTimeout(() => {
            const data = 'Data fetched';
            resolve(data);
        }, 1000);
    });
}

fetchData()
  .then((data) => {
        console.log(data);
    })
  .catch((error) => {
        console.error(error);
    });
  • async/await:基于 Promise 实现的异步编程语法糖,使异步代码看起来更像同步代码。

javascript

function fetchData() {
    return new Promise((resolve) => {
        setTimeout(() => {
            const data = 'Data fetched';
            resolve(data);
        }, 1000);
    });
}

async function getData() {
    try {
        const data = await fetchData();
        console.log(data);
    } catch (error) {
        console.error(error);
    }
}

getData();

30. 浏览器缓存

面试题:解释浏览器缓存的类型、原理和作用。
答案解析

  • 类型

    • 强缓存:浏览器直接从本地缓存中读取资源,不需要向服务器发送请求。通过 Expires 和 Cache-Control 头字段控制。
    • 协商缓存:浏览器在使用本地缓存之前,会先向服务器发送一个请求,询问服务器该资源是否有更新。如果服务器返回 304 状态码,则表示资源没有更新,浏览器可以使用本地缓存;如果服务器返回新的资源,则浏览器会使用新的资源。通过 Last-Modified 和 ETag 头字段控制。
  • 原理

    • 强缓存Expires 指定资源的过期时间,Cache-Control 可以设置更精细的缓存策略,如 max-age 表示资源的有效时间。
    • 协商缓存Last-Modified 记录资源的最后修改时间,ETag 是资源的唯一标识符。浏览器在请求时会携带 If-Modified-Since 或 If-None-Match 头,服务器根据这些信息判断资源是否有更新。
  • 作用

    • 减少服务器负载:减少了对服务器的请求,降低了服务器的压力。
    • 提高页面加载速度:直接从本地缓存读取资源,加快了页面的加载速度。

31. 什么是单线程,和异步的关系

面试题:解释单线程的概念,以及它与异步编程的关系。
答案解析

  • 单线程概念:单线程是指在同一时间只能执行一个任务。JavaScript 是单线程的,这意味着在浏览器环境中,同一时间只能有一个 JavaScript 代码块在执行。
  • 与异步编程的关系:由于 JavaScript 是单线程的,如果所有任务都同步执行,当遇到耗时的操作(如网络请求、文件读取等)时,会阻塞后续代码的执行,导致页面卡顿。而异步编程可以在不阻塞主线程的情况下处理耗时操作,当异步操作开始后,主线程可以继续执行后续代码,等异步操作完成后,通过回调函数、Promise 等方式通知主线程处理结果。因此,异步编程是解决单线程环境下性能问题的重要手段。

32. 说说 event Loop (事件循环)

面试题:简述事件循环的工作原理和作用。
答案解析

  • 工作原理

    • 任务队列:包括宏任务队列和微任务队列。宏任务包括 setTimeoutsetIntervalI/O 操作等;微任务包括 Promise.thenMutationObserver 等。

    • 事件循环过程

      1. 从宏任务队列中取出一个宏任务执行。
      2. 执行完一个宏任务后,检查微任务队列,如果微任务队列中有任务,则依次执行微任务队列中的所有任务,直到微任务队列为空。
      3. 重复步骤 1 和 2,不断循环执行。
  • 作用:事件循环是 JavaScript 实现异步编程的核心机制,它负责处理异步任务的回调函数,确保异步任务能够在合适的时机执行,同时避免阻塞主线程,提高了程序的性能和响应性。

33. 描述浏览器的渲染过程,DOM 树和渲染树的区别

面试题:请描述浏览器的渲染过程,并说明 DOM 树和渲染树的区别。
答案解析

  • 浏览器渲染过程

    1. 解析 HTML:浏览器将 HTML 代码解析成 DOM 树。
    2. 解析 CSS:将 CSS 代码解析成 CSSOM 树。
    3. 合并 DOM 树和 CSSOM 树:将 DOM 树和 CSSOM 树合并成渲染树(Render Tree)。
    4. 布局:计算渲染树中每个节点的位置和大小。
    5. 绘制:将渲染树中的节点绘制到屏幕上。
  • DOM 树和渲染树的区别

    • DOM 树:是由 HTML 元素组成的树形结构,它包含了页面的所有元素信息,但不包含元素的样式信息。
    • 渲染树:是由 DOM 树和 CSSOM 树合并而成的,它只包含可见元素,并且包含了元素的样式信息。渲染树的节点与 DOM 树的节点并不一一对应,例如 display: none 的元素不会出现在渲染树中。

34. Js 内存泄漏及垃圾回收方法

面试题:解释 JavaScript 内存泄漏的概念、常见原因和垃圾回收方法。
答案解析

  • 内存泄漏概念:内存泄漏是指程序在运行过程中,由于某些原因导致一些内存无法被释放,随着时间的推移,内存占用会不断增加,最终可能导致程序崩溃。

  • 常见原因

    • 意外的全局变量:在函数内部没有使用 varlet 或 const 声明的变量会成为全局变量,不会被垃圾回收。
    • 未清除的定时器和回调函数:如果定时器或回调函数一直存在,并且引用了一些对象,这些对象就不会被垃圾回收。
    • 闭包:闭包会持有对外部函数作用域中变量的引用,如果闭包一直存在,这些变量就不会被垃圾回收。
  • 垃圾回收方法

    • 标记清除算法:这是最常见的垃圾回收算法。垃圾回收器会从根对象(如全局对象)开始,标记所有可以访问的对象,然后清除所有未标记的对象。
    • 标记整理算法:在标记清除算法的基础上,标记整理算法会在清除未标记对象后,对存活的对象进行整理,将它们移动到内存的一端,以减少内存碎片。

35. 列举一下 JavaScript 数组和对象有哪些原生方法?

面试题:请列举至少五个 JavaScript 数组的原生方法和三个 JavaScript 对象的原生方法。
答案解析

  • 数组原生方法

    • push() :向数组的末尾添加一个或多个元素,并返回新的长度。
    • pop() :移除数组的最后一个元素,并返回该元素。
    • shift() :移除数组的第一个元素,并返回该元素。
    • unshift() :向数组的开头添加一个或多个元素,并返回新的长度。
    • splice() :可以用于删除、插入或替换数组中的元素。
    • map() :创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
    • filter() :创建一个新数组,其包含通过所提供函数实现的测试的所有元素。
  • 对象原生方法

    • Object.keys() :返回一个由对象的可枚举属性组成的数组。
    • Object.values() :返回一个由对象的可枚举属性的值组成的数组。
    • Object.assign() :用于将一个或多个源对象的所有可枚举属性复制到目标对象。

36. MVVM 脏数据检测 数据劫持 Proxy 与 Obeject.defineProperty 对比

概念解释

  • MVVM(Model - View - ViewModel)

    • MVVM 是一种前端开发模式,它将视图(View)和数据模型(Model)进行分离,通过 ViewModel 来实现两者之间的双向数据绑定。ViewModel 作为 View 和 Model 的桥梁,负责监听 Model 的变化并更新 View,同时也监听 View 的操作并更新 Model。这种模式使得开发者可以专注于数据和业务逻辑的处理,而无需手动操作 DOM,提高了开发效率和代码的可维护性。例如在 Vue.js 框架中就广泛应用了 MVVM 模式。
  • 脏数据检测

    • 脏数据检测是一种用于检测数据是否发生变化的机制。在 MVVM 模式中,为了实现数据的双向绑定,需要知道数据何时发生了改变,以便及时更新视图。脏数据检测会定期检查数据的状态,如果发现数据与之前的状态不同,就认为数据是 “脏” 的,然后触发相应的更新操作。Angular 1.x 版本采用的就是脏数据检测机制,它会在每个事件循环结束时检查所有绑定的数据。
  • 数据劫持

    • 数据劫持是指在访问或修改对象的属性时,进行拦截并执行额外的操作。在 MVVM 模式中,数据劫持常用于实现数据的双向绑定。通过劫持对象属性的 getter 和 setter 方法,可以在数据发生变化时自动更新视图,或者在视图发生变化时更新数据。Vue.js 2.x 版本使用 Object.defineProperty() 方法实现了数据劫持。

Proxy 与 Object.defineProperty 的对比

优点对比
  • Proxy

    • 可拦截多种操作Proxy 可以拦截对象的多种操作,如属性的读取、设置、删除、函数调用等。除了 get 和 set 之外,还能拦截 hasdeletePropertyownKeys 等方法,提供了更强大的元编程能力。

    javascript

    const target = { name: 'John' };
    const handler = {
        get(target, property) {
            console.log(`Getting property ${property}`);
            return target[property];
        },
        set(target, property, value) {
            console.log(`Setting property ${property} to ${value}`);
            target[property] = value;
            return true;
        },
        deleteProperty(target, property) {
            console.log(`Deleting property ${property}`);
            delete target[property];
            return true;
        }
    };
    const proxy = new Proxy(target, handler);
    console.log(proxy.name); // 触发 get 拦截
    proxy.age = 30; // 触发 set 拦截
    delete proxy.name; // 触发 deleteProperty 拦截
    
    • 可拦截整个对象Proxy 可以直接对整个对象进行拦截,而不需要像 Object.defineProperty() 那样对每个属性进行单独处理。对于动态添加的属性,Proxy 也能自动拦截,无需额外的处理。

    javascript

    const target = {};
    const handler = {
        set(target, property, value) {
            console.log(`Setting new property ${property} to ${value}`);
            target[property] = value;
            return true;
        }
    };
    const proxy = new Proxy(target, handler);
    proxy.newProp = 'new value'; // 动态添加属性,可被拦截
    
  • Object.defineProperty

    • 浏览器兼容性好Object.defineProperty() 是 ES5 中引入的方法,在大多数浏览器中都有良好的支持,包括一些旧版本的浏览器。这使得在一些对兼容性要求较高的项目中,Object.defineProperty() 是一个更可靠的选择。
缺点对比
  • Proxy

    • 浏览器兼容性较差Proxy 是 ES6 中引入的新特性,在一些旧版本的浏览器中不被支持,需要进行额外的兼容处理或者使用垫片库。
    • 性能开销相对较大:由于 Proxy 提供了更强大的功能,其实现相对复杂,可能会带来一定的性能开销。在对性能要求极高的场景下,可能需要谨慎使用。
  • Object.defineProperty

    • 无法拦截新增和删除属性Object.defineProperty() 只能对已经存在的属性进行拦截,对于动态添加或删除的属性无法自动拦截。在 Vue.js 2.x 中,如果要响应式地处理动态添加的属性,需要使用 Vue.set() 或 this.$set() 方法。
    • 只能劫持对象的属性Object.defineProperty() 只能劫持对象的属性,对于数组的一些方法(如 pushpop 等)无法直接拦截,需要对数组的方法进行重写才能实现响应式。而 Proxy 可以直接拦截数组的操作。

    javascript

    const arr = [1, 2, 3];
    const arrProxy = new Proxy(arr, {
        set(target, property, value) {
            console.log(`Array property ${property} set to ${value}`);
            target[property] = value;
            return true;
        }
    });
    arrProxy.push(4); // 可拦截数组的 push 操作