this 面试一网打尽

this 到底是什么?

《你不知道的JavaScript》上的原话:

我的理解:

this 是一个在函数执行时动态绑定 的关键字,本质上是一个 JavaScript 运行时上下文(Execution Context)中的一个特殊属性,当我们使用this时,它会指向一个对象,帮助函数访问这个对象里的属性和方法


在 JavaScript 中,执行一段代码时,会创建一个执行上下文(Execution Context) ,其中包含:

  • 变量环境(Variable Environment)
  • 词法环境(Lexical Environment)
  • this 绑定

当 JavaScript 执行一个函数时,它会创建一个新的执行上下文,并将一个特定的对象赋值给 this。这个对象可能是:

  • 全局对象 (如 windowglobal
  • 调用该函数的对象
  • 手动绑定的对象
  • 新创建的对象(在 new 关键字下)
  • 箭头函数继承的外部 this

this 并不是变量 ,也不是作用域的一部分,而是执行上下文中的一个特殊属性。

这么复杂的this 有啥用?

还是那本熟悉的小黄书,它说:

JavaScript 设计 this 的初衷是为了提供一种灵活的方式来访问调用者(即调用函数的对象)的属性和方法,而不需要显式地将对象传递给函数。这使得代码更加简洁和通用。

举个例子:

js 复制代码
const person = {
  name: "Alice",
  greet: function () {
    console.log("Hello, my name is " + this.name);
  },
};

person.greet(); // 输出: Hello, my name is Alice

在这个例子中,this 指向 person 对象。通过使用 this,我们可以在 greet 方法中直接访问 name 属性,而不需要显式地写成 person.name。如果将来我们将 greet 方法复制到其他对象上,this 会自动指向新的对象,而不是硬编码为某个特定对象。

this在全局环境下的指向

在大多数情况下,this都是出现在函数中的。

浏览器环境

在浏览器中,全局对象是 window 。因此,在全局作用域下直接使用 this 时:

  • 非严格模式this 指向全局对象 window
  • 严格模式this 仍然指向全局对象 window
js 复制代码
console.log(this)
console.log(window)
console.log(this === window)

Node.js 环境

在 Node.js 中,全局对象是 global 。然而,在模块化的代码中,每个文件被当作一个独立的模块,this 在顶层作用域下指向的是当前模块的 module.exports 对象,而不是 global

在 Node.js 的全局作用域中:

  • 非严格模式this 指向 module.exports
  • 严格模式this 也指向 module.exports
js 复制代码
console.log(this)
console.log(global)
console.log(this === global)
console.log(this === module.exports)

this的四种绑定规则

默认绑定

独立的函数调用时,this会指向全局对象

  • 浏览器 环境下,全局对象是 window
  • Node.js 环境下,全局对象是 global
js 复制代码
function foo1(){
    console.log(this)
}
function foo2(){
    console.log(this)
    foo1()
}
function foo3(){
     console.log(this) 
     foo2()
}
     foo3()

这三个函数全部都是不带任何修饰的独立调用,所以全部都是指向window

js 复制代码
var obj = {
    name: 'obj',
    foo1: function () {
        console.log(this)
    },
}
var bar = obj.foo1;
bar()

bar就是对函数foo1的一个引用,我不管你foo1是在obj里面还是在全局作用域下定义的,bar函数运行时是纯粹的独立调用,所以还是this指向window。

js 复制代码
function foo(){
    function bar(){
        console.log(this)
    }
    return bar
}
const fn = foo()
fn()// window

严格模式("use strict")下:

js 复制代码
"use strict";
function strictFunction() {
    console.log(this); // undefined
}
strictFunction();

在严格模式下,函数中的 this 会变成 undefined,而不是 windowglobal


隐式绑定(对象调用)

如果函数是通过某个对象调用的,this 绑定到该对象,记住这个XXX.fun()。:

js 复制代码
function foo() {
   console.log( this.a );
}
var obj = {
   a: 2,
   foo: foo
}
var a = 3;
obj.foo(); // 2

在这里,this 指向 obj,因为 sayHello() 是被 obj 调用的。

需要注意的是:对象属性链中只有最后一层会影响到调用位置。

js 复制代码
function foo() {
    console.log( this.a );
}
var obj2 = {
    a: 42,
    foo: foo
};
var obj1 = {
    a: 2,
    obj2: obj2
};
obj1.obj2.foo(); // 42

结果是:42

因为只有最后一层会确定this指向的是什么,不管有多少层,在判断this的时候,我们只关注最后一层,即此处的obj2。

注意:丢失隐式绑定

js 复制代码
var name = 'Bob'
const obj = {
    name: "Alice",
    sayHello: function() {
        console.log(this.name);
    }
};
obj.sayHello(); // 输出: Alice
const say = obj.sayHello;
say(); // 输出: Bob

其实之前就说过了这个例子,这里 say() 作为普通函数调用,是一个不带任何修饰的函数调用,因此应用了默认绑定。


显式绑定(callapplybind

callapply
  • callapply 可以手动设置 this,并立即执行函数。
  • 区别是 call 逐个传参,而 apply 传入一个数组。
js 复制代码
function greet(age) {
    console.log(`My name is ${this.name}, I am ${age} years old.`);
}

const person = { name: "Bob" };

greet.call(person, 25);   // My name is Bob, I am 25 years old.
greet.apply(person, [25]); // My name is Bob, I am 25 years old.

除了直接传递一个对象,也可以传入一个基本数据类型,call和apply内部会帮包装成对象。

js 复制代码
function sum(num1,num2,num3){
    console.log(num1+num2,this)
}
sum.call('call',1,2,3)
sum.apply('apply',[1,2,3])
sum.call(undefined,1,2,3)
sum.apply(null,[1,2,3])

传入null和undefined时会采用默认绑定规则。

但是在严格模式下:既不会帮我包装对象,也不会采用默认绑定。

bind
  • bind 也是手动设置 this,但不会立即执行函数,而是返回一个新的绑定了 this 的函数
js 复制代码
function foo(){
    console.log(this)
}
var bar = foo.bind('bind')
bar()
bar()
bar()

显示绑定的优先级比默认绑定更高,之后每次调用bar,都是:


new 绑定(构造函数)

当函数被 new 调用时:

  1. JavaScript 会创建一个新的对象
  2. this 绑定到新创建的对象。
js 复制代码
function Person(name) {
    this.name = name;
}
const p = new Person("Charlie");
console.log(p.name); // Charlie

API中的this

js 复制代码
setTimeout(function() {
    console.log(this); // 在浏览器中是 window
}, 1000);

可以把它理解为:

在 DOM 事件监听器中,this 默认指向绑定事件的 DOM 元素。

js 复制代码
document.getElementById('myButton').addEventListener('click', function () {
    console.log(this); // 输出:<button id="myButton">...</button>
});

数组上的forEach/map/filter能绑定this

js 复制代码
var arr = [1, 2, 3]
arr.forEach(function (item) {
    console.log(item,this)
})
arr.forEach(function (item) {
    console.log(item,this)
},'hello')

new和bind的优先级

new不能直接与 callapply 一起使用

你不能这样写:

arduino 复制代码
new obj.call(...)
new obj.apply(...)

原因

  1. new 需要一个构造函数,而 call/apply 返回的是函数调用的结果,不是构造函数
  2. 语法上这是不允许的,会直接抛出语法错误

可以与 bind 一起使用

你可以这样使用:

go 复制代码
new (func.bind(thisArg, arg1, arg2))

为什么可以

bind 返回的是一个绑定函数,这个函数可以作为构造函数使用

执行顺序

  1. bind 先执行bind 方法首先创建一个新的绑定函数
  2. new 后执行 :然后 new 操作符作用于这个绑定函数

优先级

  • new 的优先级高于 bindthis 绑定

    • 当使用 new 调用绑定的函数时,bind 设置的 this 值会被完全忽略
    • new 会按照常规规则创建一个新对象作为 this
js 复制代码
function Person() {
    console.log("this is:", this);
}

const BoundPerson = Person.bind({ foo: 1 });

// 普通调用 - 使用 bind 的 this
BoundPerson(); // 输出: this is: { foo: 1 }

// new 调用 - 忽略 bind 的 this
new BoundPerson(); // 输出: this is: Person {}  就是我们说的新创建的对象
  • bind 的参数绑定仍然有效

    • bind 预先绑定的参数会被保留
    • 调用时new传入的参数会追加在预先绑定的参数之后

bind 预先绑定的参数是永久固定的 ,无法通过后续调用更改。这是 bind 的核心特性之一。

js 复制代码
function sum(a, b, c) {
  return a + b + c;
}

const boundSum = sum.bind(null, 1, 2); // 永久绑定 a=1, b=2
console.log(boundSum(3)); // 1+2+3 = 6
// 无法改变已绑定的1和2

当与 new 一起使用时:

  • 绑定的参数仍然不可更改
  • 新传入的参数会追加在已绑定参数之后
js 复制代码
function Person(name, age, country) {
  this.name = name;
  this.age = age;
  this.country = country;
}

const BoundPerson = Person.bind(null, 'John', 30);
const p1 = new BoundPerson();       // Person {name: 'John', age: 30, country: undefined}
const p2 = new BoundPerson('USA');  // Person {name: 'John', age: 30, country: 'USA'}

特殊情况

如果绑定的函数本身没有使用 this(不是构造函数风格),new 仍然会创建一个对象,但可能不是你期望的:

js 复制代码
function sum(a, b) {
  return a + b;
}

const BoundSum = sum.bind(null, 2);
const result = new BoundSum(3); // 奇怪但合法的用法

console.log(result); 
// sum {} (一个无意义的对象,因为 sum 没有使用 this)
// 但 sum 仍然执行了,返回了 5,只是返回值被 new 忽略了

优先级从高到低:

  1. new 绑定 - 当使用构造函数创建对象实例时,this 绑定到新创建的实例。
  2. 显式绑定 - 使用 callapplybind 方法时,this 被明确指定。
  3. 隐式绑定 - 函数作为对象的方法调用时,this 绑定到该对象。
  4. 默认绑定 - 在非严格模式下指向全局对象(如 window),在严格模式下为 undefined

例外情况

间接函数引用

js 复制代码
var obj1 = {
    name: 'obj1',
    foo: function () {
        console.log(this)
    }
}

var obj2 = { name: 'obj2' }
obj2.foo = obj1.foo;
obj2.foo()

隐式绑定,输出obj2,相信大家都知道,但是如果是这样呢:

javascript 复制代码
var obj1 = {
    name: 'obj1',
    foo: function () {
        console.log(this)
    }
}
var obj2 = {
    name: 'obj2',
};

(obj2.foo = obj1.foo)()

(obj2.foo = obj1.foo) 的结果是一个独立的函数引用,因此调用位置是foo(),而不是绑定到某个对象的方法,所以最后采用默认绑定。

箭头函数的this绑定

在箭头函数中,this 的指向是定义时 决定的,而不是调用时 决定的。这与普通函数不同,普通函数的 this 是在调用时动态绑定的。

箭头函数 this 的规则

继承外层作用域的 this(词法作用域)

  • 箭头函数不会创建自己的 this,它会继承定义它的作用域 中的 this
  • 这个作用域通常是它的外层函数,或者是全局作用域(如果箭头函数是在全局作用域中定义的)。

不能通过 callapplybind 改变 this

  • 对于普通函数,我们可以使用 callapplybind 显式改变 this,但箭头函数不受这些方法影响。
  1. 不能作为构造函数
  • 由于箭头函数没有自己的 this,因此不能使用 new 关键字实例化对象,否则会报错。

  1. 继承外层 this
js 复制代码
const obj = {
  name: "Alice",
  sayHello: function () {
    const arrowFn = () => {
      console.log(this.name); // 继承 sayHello 方法的 this(即 obj)
    };
    arrowFn();
  },
};
obj.sayHello(); // Alice

这里 arrowFn 继承了 sayHello 方法的 this,因此 this.name 指向 obj.name

  1. call / apply / bind 无效
js 复制代码
const obj = { name: "Alice" };

const arrowFn = () => {
  console.log(this); // this 继承自定义它的作用域(通常是 window 或 global)
};

arrowFn.call(obj); // 依然是 window 或 global,不是 obj
  1. 不能作为构造函数
js 复制代码
const Arrow = () => {};
const obj = new Arrow(); // TypeError: Arrow is not a constructor
  1. setTimeout 中的行为
js 复制代码
const obj = {
  name: "Alice",
  sayHello: function () {
    setTimeout(() => {
      console.log(this.name); // this 继承自 sayHello 方法
    }, 1000);
  },
};
obj.sayHello(); // Alice

为什么? 因为箭头函数不会创建自己的 this,所以 setTimeout 里的 this 依然是 sayHello 方法的 this,即 obj

练习

js 复制代码
    <script>
        var name = "window";
        var person = {
            name: "person",
            sayName: function () {
                console.log(this.name);

            }
        }
        function sayName() {
            var sss = person.sayName;
            sss();  
            //window  独立函数调用 默认绑定
            person.sayName(); 
            //person 隐式绑定
            (person.sayName)();
            //其实就是person.sayName() 隐式绑定
            (b = person.sayName)();
            //间接函数引用 默认绑定
        }
        sayName();
       
        //person
        //person
        //window    
    </script>
js 复制代码
 <script>
        var name = 'window'
        var person1 = {
            name: 'peson1',
            foo1: function () {
                console.log(this.name)
            },
            foo2: () => console.log(this.name),

            foo3: function () {
                return function () {
                    console.log(this.name)
                }
            },
            foo4: function () {
                return () => {
                    console.log(this.name)
                }
            }
        }


        var person2 = { name: 'person2' }
        person1.foo1();//person1 隐式绑定
        person1.foo1.call(person2);//person2  显示绑定大于隐式绑定

        person1.foo2()//window  箭头函数继承的this是全局作用域
        person1.foo2.call(person2);//window 箭头函数无法被显示绑定

        person1.foo3()()//window 理解为person1.foo3()返回的是一个普通函数fn,然后fn(),默认绑定
        person1.foo3.call(person2)()//window  foo3()函数被显示绑定,但是foo3()函数返回的是一个普通函数,所以普通函数的this指向window
        person1.foo3().call(person2)//person2 最终调用返回函数式,使用的是显示绑定

        person1.foo4()()//person1 箭头函数不绑定this,上层作用域this是personl
        person1.foo4.call(person2)();//person2 上层作用域被显示的绑定了person2
        person1.foo4().call(person2);//person1 上层作用域已经被显示的绑定了person1,call无法改变箭头this的指向

总结

最后还是用《你不知道的JavaScript》中的秘诀来结尾。

希望能帮到正在面试的你。

相关推荐
猫猫不是喵喵.4 小时前
vue 路由
前端·javascript·vue.js
烛阴4 小时前
JavaScript Import/Export:告别混乱,拥抱模块化!
前端·javascript
bin91534 小时前
DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加行拖拽排序功能示例12,TableView16_12 拖拽动画示例
前端·javascript·vue.js·ecmascript·deepseek
拉不动的猪5 小时前
vue自定义“权限控制”指令
前端·javascript·vue.js
魔云连洲6 小时前
Vue2和Vue3响应式的基本实现
开发语言·前端·javascript·vue.js
CreatorRay6 小时前
受控组件和非受控组件的区别
前端·javascript·react.js
JSON_L7 小时前
Vue 组件通信 - Ref组件通信
javascript·vue.js·ecmascript
Fri_8 小时前
Vue 使用 xlsx 插件导出 excel 文件
javascript·vue.js·excel
黑贝是条狗8 小时前
html 列表循环滚动,动态初始化字段数据
前端·javascript·html
萌萌哒草头将军8 小时前
🔥🔥🔥4 月 1 日尤雨溪突然宣布使用 Go 语言重写 Rolldown 和 Oxc!
前端·javascript·vue.js