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 引擎需要查找一个变量时,它会按照以下步骤进行:
- 从当前执行上下文的变量对象开始查找:首先,引擎会在当前执行上下文的变量对象中查找该变量。如果找到了,就返回该变量的值。
- 沿着作用域链向上查找:如果在当前执行上下文的变量对象中没有找到该变量,引擎会沿着作用域链向上查找,依次检查外部作用域的变量对象。直到找到该变量或者到达作用域链的末尾(全局作用域)。
- 变量未找到:如果在整个作用域链中都没有找到该变量,引擎会抛出
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 实现异步编程的核心机制,它负责处理异步任务的回调函数。
- 宏任务:包括
setTimeout、setInterval、setImmediate(Node.js 环境)、I/O 操作、UI 渲染等。宏任务会被放入宏任务队列中。 - 微任务:包括
Promise.then、MutationObserver、process.nextTick(Node.js 环境)等。微任务会被放入微任务队列中。
执行顺序:
- 从宏任务队列中取出一个宏任务执行。
- 执行完一个宏任务后,检查微任务队列,如果微任务队列中有任务,则依次执行微任务队列中的所有任务,直到微任务队列为空。
- 重复步骤 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 的基本数据类型和引用数据类型,并说明它们在内存中的存储方式。
答案解析:
- 基本数据类型:包括
number、string、boolean、null、undefined、symbol和bigint。基本数据类型的值直接存储在栈内存中,它们的访问速度较快。 - 引用数据类型:包括
object(如Array、Function、Date等)。引用数据类型的值存储在堆内存中,而在栈内存中存储的是该对象在堆内存中的引用地址。通过引用地址可以访问堆内存中的对象。
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
call、apply、bind方法:可以显式地指定this的指向。
function greet() {
console.log(`Hello, ${this.name}`);
}
const person1 = { name: 'Bob' };
greet.call(person1); // Hello, Bob
14. new 操作符具体干了什么呢?
面试题:简述 new 操作符在创建对象时的具体步骤。
答案解析:
当使用 new 操作符调用一个构造函数时,会按以下步骤执行:
- 创建新对象:创建一个全新的空对象。
- 设置原型:将新对象的
[[Prototype]]属性设置为构造函数的prototype属性,使新对象能够继承构造函数原型上的属性和方法。 - 执行构造函数:以新对象作为
this的值执行构造函数,并将传入new操作符的参数传递给构造函数。 - 返回对象:如果构造函数返回一个对象,则返回该对象;否则返回新创建的对象。
示例代码如下:
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 '&';
case '<': return '<';
case '>': return '>';
case '"': return '"';
case "'": return ''';
}
});
}
- 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存在的变量提升和作用域问题。 - 箭头函数:简化了函数的定义,并且没有自己的
this、arguments、super或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 (事件循环)
面试题:简述事件循环的工作原理和作用。
答案解析:
-
工作原理:
-
任务队列:包括宏任务队列和微任务队列。宏任务包括
setTimeout、setInterval、I/O操作等;微任务包括Promise.then、MutationObserver等。 -
事件循环过程:
- 从宏任务队列中取出一个宏任务执行。
- 执行完一个宏任务后,检查微任务队列,如果微任务队列中有任务,则依次执行微任务队列中的所有任务,直到微任务队列为空。
- 重复步骤 1 和 2,不断循环执行。
-
-
作用:事件循环是 JavaScript 实现异步编程的核心机制,它负责处理异步任务的回调函数,确保异步任务能够在合适的时机执行,同时避免阻塞主线程,提高了程序的性能和响应性。
33. 描述浏览器的渲染过程,DOM 树和渲染树的区别
面试题:请描述浏览器的渲染过程,并说明 DOM 树和渲染树的区别。
答案解析:
-
浏览器渲染过程:
- 解析 HTML:浏览器将 HTML 代码解析成 DOM 树。
- 解析 CSS:将 CSS 代码解析成 CSSOM 树。
- 合并 DOM 树和 CSSOM 树:将 DOM 树和 CSSOM 树合并成渲染树(Render Tree)。
- 布局:计算渲染树中每个节点的位置和大小。
- 绘制:将渲染树中的节点绘制到屏幕上。
-
DOM 树和渲染树的区别:
- DOM 树:是由 HTML 元素组成的树形结构,它包含了页面的所有元素信息,但不包含元素的样式信息。
- 渲染树:是由 DOM 树和 CSSOM 树合并而成的,它只包含可见元素,并且包含了元素的样式信息。渲染树的节点与 DOM 树的节点并不一一对应,例如
display: none的元素不会出现在渲染树中。
34. Js 内存泄漏及垃圾回收方法
面试题:解释 JavaScript 内存泄漏的概念、常见原因和垃圾回收方法。
答案解析:
-
内存泄漏概念:内存泄漏是指程序在运行过程中,由于某些原因导致一些内存无法被释放,随着时间的推移,内存占用会不断增加,最终可能导致程序崩溃。
-
常见原因:
- 意外的全局变量:在函数内部没有使用
var、let或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()方法实现了数据劫持。
- 数据劫持是指在访问或修改对象的属性时,进行拦截并执行额外的操作。在 MVVM 模式中,数据劫持常用于实现数据的双向绑定。通过劫持对象属性的
Proxy 与 Object.defineProperty 的对比
优点对比
-
Proxy
- 可拦截多种操作:
Proxy可以拦截对象的多种操作,如属性的读取、设置、删除、函数调用等。除了get和set之外,还能拦截has、deleteProperty、ownKeys等方法,提供了更强大的元编程能力。
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()只能劫持对象的属性,对于数组的一些方法(如push、pop等)无法直接拦截,需要对数组的方法进行重写才能实现响应式。而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 操作 - 无法拦截新增和删除属性: