阅读视图

发现新文章,点击刷新页面。

【JavaScript面试题-this 绑定】请说明 `this` 在不同场景下的指向(默认、隐式、显式、new、箭头函数)。

今天我们来聊一聊 JavaScript 中一个既基础又让人头疼的概念——this

一、this 是什么?

简单来说,this 是函数执行时内部自动生成的一个对象,它指向调用该函数的上下文。你可以把它理解为函数内部的“环境变量”,代表了当前函数运行时所处的对象。

一个形象的比喻

想象一下,你有一个“自我介绍”的功能,不同的人调用它时,“我”这个字指向不同的人:

  • 小明说“叫小明”,这里的“我”就是小明。
  • 小红说“叫小红”,这里的“我”就是小红。

在 JavaScript 中,this 就像这句话里的“我”,而那个自我介绍的函数就像一句模板:“我叫 xxx”。这个模板里的 this.name 会根据是谁在调用而自动替换成对应的人名。

用代码表示:

javascript

function introduce() {
  console.log(`我叫 ${this.name}`);
}

const ming = { name: '小明', introduce };
const hong = { name: '小红', introduce };

ming.introduce(); // 我叫 小明(this 指向 ming)
hong.introduce(); // 我叫 小红(this 指向 hong)

这里的 introduce 函数内部的 this 就像“我”一样,随着调用者(ming 或 hong)不同,指向也不同。这就是 this 的动态性——它是在函数执行时,根据调用它的对象确定的。

二、this 能做什么?

理解了 this 是动态上下文,那么它能为我们做什么呢?

  • 让同一个函数服务于不同的对象,实现代码复用;
  • 在构造函数中初始化实例属性
  • 在事件处理中方便地访问触发元素
  • 显式地指定上下文,借用其他对象的方法
  • 在回调函数中优雅地保留外层 this

下面我们就通过一个个实战场景,来体会 this 的妙用。


三、实战场景一网打尽

场景1:对象方法中的 this —— 隐式绑定

假设我们有一个用户对象,需要输出用户的名称:

javascript

const user1 = {
  name: '小明',
  greet() {
    console.log(`大家好,我是 ${this.name}`);
  }
};

user1.greet(); // 大家好,我是 小明

当 greet 作为 user1 的方法被调用时,this 指向 user1,所以能正确访问 name

能做什么:我们可以定义多个类似的对象,使用同一个方法结构,轻松访问各自的数据。

陷阱:如果把方法赋值给一个变量再调用,this 就会丢失:

javascript

const fn = user1.greet;
fn(); // 大家好,我是 undefined (非严格模式下 this 指向 window,没有 name 属性)

解决方法:使用 bind 强制绑定 this,或者用箭头函数(后面会讲)。


场景2:构造函数中的 this —— new 绑定

在面向对象编程中,我们经常用构造函数来创建对象:

javascript

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.intro = function() {
    console.log(`我叫 ${this.name},今年 ${this.age} 岁。`);
  };
}

const p1 = new Person('小红', 20);
p1.intro(); // 我叫 小红,今年 20 岁。

当使用 new 调用 Person 时,this 指向新创建的空对象,然后我们往这个对象上添加属性,最后返回这个对象。

能做什么:轻松批量创建结构相似的对象,并且每个对象的方法都能正确访问自己的属性。

注意:如果忘记写 newthis 会指向全局对象,导致全局变量污染。所以构造函数通常首字母大写,提醒自己用 new 调用。


场景3:DOM 事件处理中的 this

在浏览器中处理事件时,this 通常指向触发事件的 DOM 元素:

html

<button id="myBtn">点我</button>
<script>
  const btn = document.getElementById('myBtn');
  btn.addEventListener('click', function() {
    console.log(this); // <button id="myBtn">点我</button>
    this.textContent = '已点击';
  });
</script>

能做什么:在事件回调中直接通过 this 操作当前元素,非常方便。

注意:如果回调使用箭头函数,this 就会指向外层作用域(比如 window),无法直接操作元素。所以事件回调一般用普通函数。


场景4:显式指定 this —— call / apply / bind

有时候我们需要手动指定函数的 this,比如“借用”其他对象的方法。

javascript

const user2 = { name: '小刚' };
const user3 = { name: '小丽' };

function introduce(hobby) {
  console.log(`我是 ${this.name},喜欢 ${hobby}`);
}

introduce.call(user2, '篮球'); // 我是 小刚,喜欢 篮球
introduce.apply(user3, ['跳舞']); // 我是 小丽,喜欢 跳舞

const introduceXiaoGang = introduce.bind(user2, '足球');
introduceXiaoGang(); // 我是 小刚,喜欢 足球
  • call 和 apply 立即调用函数,区别是传参方式不同。
  • bind 返回一个新函数,永久绑定 this,可用于后续调用。

能做什么:实现函数复用,动态改变上下文;也可以用于“函数借用”,比如数组方法借用给类数组对象。


场景5:回调函数中保持 this —— 箭头函数的妙用

在异步回调或定时器中,我们经常需要访问外层的 this,但普通函数的 this 会指向全局(或 undefined 严格模式),导致无法访问期望的对象。

传统解决方式是用 var self = this 缓存,或者用 bind

javascript

function Counter() {
  this.count = 0;
  setInterval(function() {
    this.count++; // 这里的 this 指向 window,无法更新 count
    console.log(this.count);
  }, 1000);
}
new Counter(); // 输出 NaN 或 undefined

用 bind 修正:

javascript

function Counter() {
  this.count = 0;
  setInterval(function() {
    this.count++;
    console.log(this.count);
  }.bind(this), 1000);
}
new Counter(); // 1 2 3 ...

而箭头函数让这一切变得简单:箭头函数没有自己的 this,它会捕获定义时外层作用域的 this

javascript

function Counter() {
  this.count = 0;
  setInterval(() => {
    this.count++; // 这里的 this 继承自 Counter 实例
    console.log(this.count);
  }, 1000);
}
new Counter(); // 1 2 3 ...

能做什么:在回调、事件监听、Promise 等场景中,优雅地保留外层 this,避免繁琐的 self = this 或 bind

注意:箭头函数的 this 一旦确定,就无法通过 call/apply/bind 改变,所以不能用于动态上下文。


场景6:嵌套函数中的 this 问题

在对象方法内部定义普通函数,这个普通函数的 this 会指向全局(或 undefined),这常常让人困惑:

javascript

const obj = {
  name: 'obj',
  foo() {
    function bar() {
      console.log(this.name);
    }
    bar(); // 非严格模式输出 undefined 或 window.name
  }
};
obj.foo();

如何让 bar 也能访问 obj 的 name?有几种方法:

  • 用箭头函数(推荐):

    javascript

    foo() {
      const bar = () => {
        console.log(this.name);
      };
      bar(); // obj
    }
    
  • 在外层保存 this

    javascript

    foo() {
      const self = this;
      function bar() {
        console.log(self.name);
      }
      bar();
    }
    
  • 用 bind

    javascript

    foo() {
      function bar() {
        console.log(this.name);
      }
      bar.bind(this)();
    }
    

能做什么:保证嵌套函数也能访问外层对象的属性,避免作用域丢失。


四、this 绑定规则优先级(一句话总结)

当多种规则同时适用时,this 的绑定优先级是:

new 绑定 > 显式绑定(call/apply/bind) > 隐式绑定(对象方法) > 默认绑定(独立调用)

箭头函数不参与这个优先级,它完全由外层作用域决定。


五、总结与思考

回到最初的问题:this 能做什么?

  • 它让函数灵活地适应不同的调用对象,实现代码复用;
  • 它在构造函数中帮助我们初始化实例;
  • 它在事件处理中方便操作当前元素;
  • 它通过显式绑定让我们能动态指定上下文;
  • 它配合箭头函数,优雅地解决了回调中的 this 保持问题。

掌握 this 的关键,不是死记硬背规则,而是在写代码时问自己:这个函数是怎么被调用的?  调用方式决定了 this 的指向。

希望这篇文章能帮你从“this 是什么”的困惑,走向“this 能做什么”的熟练应用。如果你有更多关于 this 的实战经验或疑惑,欢迎在评论区留言讨论!


最后留个思考题:下面代码的输出是什么?为什么?

javascript

const length = 10;
function fn() {
  console.log(this.length);
}
const obj = {
  length: 5,
  method(fn) {
    fn();
    arguments[0]();
  }
};
obj.method(fn, 1);

(答案:先输出 10(或 undefined),然后输出 2。因为第一次调用 fn() 是默认绑定,第二次 arguments[0]() 是隐式绑定,this 指向 arguments 对象,其 length 是传入的参数个数,即 2。)

欢迎留言你的答案和理解!我们下期再见。

#前端、#前端面试、#干货

如果这篇这篇文章对您有帮助?关注、点赞、收藏,三连支持一下。
有疑问或想法?评论区见

【JavaScript面试题-作用域与闭包】什么是闭包?闭包在实际开发中有什么应用和潜在问题(如内存泄漏)?

什么是闭包?

闭包是指一个函数能够访问并记住其词法作用域(即定义时的作用域)中的变量,即使这个函数是在其词法作用域之外被执行的。简单来说,闭包让你可以在内层函数中访问到外层函数的作用域

在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。但是,闭包通常特指那些引用了外部函数作用域中变量的内部函数。

一个形象的比喻

你出生在你家的老房子里,房子里有你的玩具、书桌、窗外的树(这些都是环境变量)。后来你长大了,搬到了新城市,但你心里永远记得老房子的样子——甚至你还能描述出玩具放在哪个抽屉里(这就是闭包)。

这里的“你”就是内部函数,老房子就是外层函数的作用域。虽然你离开了老房子(外层函数执行结束),但你依然能回忆起里面的细节(访问外层函数的变量)。

简单来说,闭包就是一个能“记住”并继续使用它出生时周围环境(变量)的函数,即使这个函数后来被拿到别的地方去执行,它也忘不掉那些“老家”的变量。

一个典型的闭包示例:

javascript

function outerFunction(x) {
  let y = 10;
  function innerFunction() {
    console.log(x + y); // innerFunction 访问了 outerFunction 的变量 x 和 y
  }
  return innerFunction;
}

const closureFunc = outerFunction(5);
closureFunc(); // 输出 15,即使 outerFunction 已经执行完毕,innerFunction 依然能记住 x 和 y

在上面的例子中,innerFunction 就是一个闭包。它“捕获”了 x 和 y,使得这些变量在 outerFunction 执行完毕后仍然存在于内存中,供 innerFunction 后续调用。

闭包的实际应用

闭包在前端开发中应用非常广泛,以下是一些常见场景:

  1. 数据私有化与封装
    通过闭包可以模拟私有变量,隐藏实现细节,只暴露有限的接口。

    javascript

    function createCounter() {
      let count = 0;
      return {
        increment: function() { count++; },
        decrement: function() { count--; },
        getCount: function() { return count; }
      };
    }
    const counter = createCounter();
    counter.increment();
    console.log(counter.getCount()); // 1,无法直接访问 count 变量
    
  2. 函数柯里化(Currying)
    利用闭包将多参数函数转换为一系列单参数函数,提高函数复用性。

    javascript

    function multiply(a) {
      return function(b) {
        return a * b;
      };
    }
    const double = multiply(2);
    console.log(double(5)); // 10
    
  3. 回调函数与事件处理
    在异步操作或事件监听中,闭包可以记住当时的环境变量。

    javascript

    for (var i = 0; i < 3; i++) {
      setTimeout(function() {
        console.log(i); // 如果不使用闭包,会输出 3,3,3
      }, 100);
    }
    // 利用闭包修复:
    for (var i = 0; i < 3; i++) {
      (function(j) {
        setTimeout(function() {
          console.log(j); // 输出 0,1,2
        }, 100);
      })(i);
    }
    
  4. 模块化模式(Module Pattern)
    在ES6模块之前,闭包常用于创建模块,管理私有状态和公共API。

    javascript

    var myModule = (function() {
      var privateVar = 0;
      function privateMethod() { /* ... */ }
      return {
        publicMethod: function() { /* 可以使用 privateVar 和 privateMethod */ }
      };
    })();
    
  5. 函数式编程中的高阶函数
    例如 Array.map()Array.filter() 中传入的回调函数也常常形成闭包,访问外部作用域。

闭包的潜在问题:内存泄漏

闭包虽然强大,但如果不加注意,可能会导致内存泄漏,因为闭包会一直持有对外部函数变量的引用,使得这些变量无法被垃圾回收(GC)。

常见的内存泄漏场景:

  • 无意的全局变量
    在函数内部意外创建的全局变量,由于被全局对象引用,永远不会被回收。

  • 未解除的事件监听器
    如果在DOM元素上绑定了事件回调,而回调中使用了外部变量(闭包),并且没有在元素移除前解绑,那么整个闭包作用域链都不会被释放,造成泄漏。

    javascript

    function attachEvent() {
      const largeData = new Array(1000000).fill('*');
      document.getElementById('btn').addEventListener('click', function() {
        console.log(largeData.length); // largeData 被闭包引用
      });
    }
    attachEvent(); // 即使元素被移除,由于事件监听未解绑,largeData 依然存在
    
  • 在循环中创建闭包并引用大对象
    如果循环内创建的闭包长期存在(例如存储到数组中或作为回调),且引用了外部作用域的大对象,可能导致大量内存无法释放。

  • 定时器或回调未清除
    未清除的 setInterval 或 setTimeout 回调中的闭包会持续持有外部变量。

如何避免内存泄漏

  1. 及时解除引用

    • 在不需要时,将闭包变量设置为 null,切断引用。
    • 移除DOM元素前,先移除其绑定的事件监听器(removeEventListener)。
    • 清除定时器(clearInterval / clearTimeout)。
  2. 使用弱引用(WeakMap/WeakSet)
    当需要缓存数据但不想阻止垃圾回收时,可以使用 WeakMap 或 WeakSet,它们不会增加引用计数。

  3. 避免不必要的闭包
    只在必要时创建闭包,尽量减少闭包引用的变量体积。例如,如果只需要外部函数中的一小部分数据,可以考虑只传递所需值,而不是整个作用域。

  4. 使用现代框架/工具辅助
    现代框架(如React、Vue)通常会自动处理事件监听和组件卸载时的清理工作,但在手动操作DOM时仍需小心。

总结

闭包是JavaScript语言的一大特色,它让函数变得极其灵活,支持数据私有化、函数式编程等高级用法。但同时,闭包也可能因持有外部引用而导致内存泄漏,开发者需要理解其工作原理,并在实际开发中注意及时清理不再需要的引用,从而编写出高效、可靠的应用。

如果这篇这篇文章对您有帮助?关注、点赞、收藏,三连支持一下。
有疑问或想法?评论区见

#前端#干货

❌