JavaScript 面试高频易错点:面向对象、this 指向与闭包详解

作者:闭包旅人时间:2026-10-23 03:06:20浏览:434

本文梳理了 JavaScript 面试中高频出现的易错考点,重点解析面向对象与面向过程的思维差异、this 关键字在不同调用场景下的指向规则(包括箭头函数特性),以及 call/apply 的作用与区别。同时深入探讨闭包的形成机制及其在变量保护与内存管理中的实际应用,帮助开发者规避常见陷阱。

1.前言

正值金三银四招聘季,各类面试题分享层出不穷。近期我临时担任面试官,为评估前端开发者的实际水平,专门编写了一套测试题并进行了面试。在准备试题及学习设计模式的过程中,我发现许多设计模式相关的知识点,正是面试中导致候选人失分的高频考点。

因此,本文旨在总结这些容易让人陷入困境的考点。以下将展开详细介绍。

面向对象与面向过程的思维差异

2.面向对象编程

关于面向对象与面向过程,我认为二者并非绝对对立,而是相辅相成的。具体应用场景需根据实际情况具体分析。

针对面向对象编程,知乎上有一个高赞回答形象地描述了其特点:

面向对象:狗.吃(屎)

面向过程:吃.(狗,屎)

虽然该例子生动,但略显粗俗。此处我将其优化为一个更优雅的示例,以说明面向对象与面向过程的区别。

需求:定义‘守候吃火锅'的行为

面向对象的思想是:守候.动作(吃火锅)

面向过程的思想是:动作(守候,吃火锅)

代码实现方面:


//面向对象
//定义人(姓名)
let People=function(name){
 this.name=name;
}
//动作
People.prototype={
 eat:function(someThing){
 console.log(`${this.name}吃${someThing}`);
 }
}
//守候是个人,所以要创建一个人(new一次People)
let shouhou=new People('守候','男',24);
shouhou.eat('火锅');

//面向过程
let eat=function(who,someThing){
 console.log(`${who}吃${someThing}`);
}
eat('守候','火锅');

结果均输出‘守候吃火锅'。但若后续需求变为‘吃饱后写代码',该如何实现?参考以下代码:


//面向对象
shouhou.coding=function(){
 console.log(this.name+'写代码');
}
shouhou.coding();
//面向过程
let coding=function(who){
 console.log(who+'写代码');
}
coding('守候');

结果同样输出‘守候写代码'。

可以看出,面向对象具有更高的灵活性和扩展性,因为它针对具体对象(如示例中的‘守候')执行动作,这些动作可自定义扩展。

而面向过程则是定义多个动作,并指定由谁执行。

关于面向对象的基础说明至此结束,其三大特性(继承、封装、多态)建议自行查阅相关资料深入学习。

JavaScript 中 this 指向的详细解析

3.this

在 JavaScript 开发中,许多开发者对 this 的指向感到困惑。实际上,理解 this 的核心原则只有一句:哪个对象调用函数,函数内部的 this 就指向哪个对象。

下面分几种情况具体讨论:

3-1.普通函数调用

若无特殊情况,this 默认指向全局对象 window。


let username='守候'
function fn(){
 alert(this.username);//undefined
}
fn();

读者可能会疑惑为何不输出‘守候',但需注意,此处声明使用的是 let,因此不会挂载到 window 对象上。

若希望输出‘守候',代码应如下编写:


var username='守候'
function fn(){
 alert(this.username);//守候
}
fn();
//---------------
window.username='守候'
function fn(){
 alert(this.username);//守候
}
fn();

3-2.对象函数调用

这一点不难理解,即哪个对象调用函数,this 就指向该对象。


window.b=2222
let obj={
 a:111,
 fn:function(){
 alert(this.a);//111
 alert(this.b);//undefined
 }
}
obj.fn();

显然,第一次调用输出 obj.a,即 111。第二次调用时,由于 obj 没有 b 属性,故输出 undefined,因为 this 指向 obj。

但需注意以下情况:


let obj1={
 a:222
};
let obj2={
 a:111,
 fn:function(){
 alert(this.a);
 }
}
obj1.fn=obj2.fn;
obj1.fn();//222

这不难理解,虽然 obj1.fn 是从 obj2.fn 赋值而来,但实际调用函数的是 obj1,因此 this 指向 obj1。

3-3.构造函数调用


let TestClass=function(){
 this.name='111';
}
let subClass=new TestClass();
subClass.name='守候';
console.log(subClass.name);//守候
let subClass1=new TestClass();
console.log(subClass1.name)//111

这也不难理解,回忆 new 操作的四个步骤即可。

但存在一个坑,虽然不常见,但有必要提及。

若在构造函数中返回一个对象,将直接返回该对象,而非构造函数执行后创建的对象。

面向对象与面向过程的思维差异

3-4.apply 和 call 调用

apply 和 call 的作用本质上是改变传入函数的 this 指向。


let obj1={
 a:222
};
let obj2={
 a:111,
 fn:function(){
  alert(this.a);
 }
}
obj2.fn.call(obj1);

此时虽然由 obj2 调用方法,但使用了 call,动态将 this 指向 obj1。相当于 obj2.fn 的执行环境变成了 obj1。关于 apply 和 call 的详细内容将在下文提及。

3-5.箭头函数调用

首先必须承认,ES6 提供的箭头函数提高了开发效率。但在箭头函数内部,没有自己的 this,箭头函数内的 this 继承自外层环境。

一个例子:


let obj={
 a:222,
 fn:function(){ 
  setTimeout(function(){console.log(this.a)})
 }
};
obj.fn();//undefined

不难发现,虽然 fn() 内部的 this 指向 obj,但传给 setTimeout 的是普通函数,其 this 指向 window。由于 window 下没有 a,因此输出 undefined。

换成箭头函数:


let obj={
 a:222,
 fn:function(){ 
  setTimeout(()=>{console.log(this.a)});
 }
};
obj.fn();//222

这次输出 222,是因为传给 setTimeout 的是箭头函数。箭头函数内部没有 this,因此向上层作用域查找。在此例中,setTimeout 的上层作用域是 fn,而 fn 内部的 this 指向 obj。因此,setTimeout 中箭头函数的 this 指向 obj,最终输出 222。

call 与 apply 的区别及应用

4.call 和 apply

call 和 apply 的作用完全相同,唯一的区别在于参数传递方式。

call 接收的参数不固定,第一个参数是函数体内 this 的指向,第二个及以后的参数依次传入。

apply 接收两个参数,第一个参数同样是函数体内 this 的指向,第二个参数是一个集合对象(数组或类数组)。


let fn=function(a,b,c){
console.log(a,b,c);
}
let arr=[1,2,3];

面向对象与面向过程的思维差异

如上面这个例子:


let obj1={
 a:222
};
let obj2={
 a:111,
 fn:function(){
  alert(this.a);
 }
}
obj2.fn.call(obj1);

call 和 apply 的两个主要用途是:

1.改变 this 的指向(将 this 从 obj2 指向到 obj1)。

2.方法借用(obj1 没有 fn,只是借用 obj2 的方法)。

闭包的形成机制与内存管理

5.闭包

闭包可能是大家感到困惑的概念,但它是必须掌握的核心知识。下面通过一个简单例子说明:


let add=(function(){
let now=0;
return {
 doAdd:function(){
 now++;
 console.log(now);
}
}
})()

然后执行几次!

面向对象与面向过程的思维差异

从上图结果可以看到,now 这个变量并没有随着函数的执行完毕而被回收,而是继续保存在内存中。

具体原因如下:刚开始进入时,因为是自动执行函数,一开始进入会自动执行这一块:

面向对象与面向过程的思维差异

然后将该对象赋值给 add。由于 add 内部的函数依赖于 now 这个变量,因此 now 不会被销毁或回收。这就是闭包的用途之一(延续变量生命周期)。由于 now 在外部无法访问,这也是闭包的另一个用途(创建局部变量,保护局部变量不被外部访问和修改)。

有人可能会质疑闭包会导致内存泄漏。但试想,若不使用闭包,则需使用全局变量。将变量放在闭包中与放在全局变量中,对内存的影响是一致的。使用闭包反而能减少全局变量的使用,因此上述例子中使用闭包更优。

6.小结

在学习设计模式时,遇到的知识点主要包含上述内容。这些也是我在群聊、社区中观察到,导致开发者掉坑较多的考点。这些知识既是开发中的常用技能,也是面试中的高频考题,建议大家深入学习。以上内容仅为简要概述,并未深入展开。若大家对文章有任何建议,欢迎指正。

好了,以上就是这篇文章的全部内容,希望本文的内容对大家的学习或工作具有一定的参考学习价值。如果有疑问,欢迎留言交流,谢谢大家对本站的支持。

作者最新文章