JS this 1 学习 + 面试题

79 阅读11分钟

学习参考链接 coderwhy官方

看有和没有this的区别

var obj = {
    name: "John",
    eating: function () {
        console.log(this.name + "吃东西")
    },
    running: function () {
        console.log(this.name + "在跑步")
    },
    studying: function () {
        console.log(this.name + "在学习")
    }
}
obj.eating();
obj.running();
obj.studying();
var info = {
    name: "John",
    eating: function () {
        console.log(info.name + "吃东西")
    },
    running: function () {
        console.log(info.name + "在跑步")
    },
    studying: function () {
        console.log(info.name + "在学习")
    }
}
info.eating();
info.running();
info.studying();
运行结果

John吃东西 John在跑步 John在学习 John吃东西 John在跑步 John在学习

不用this有什么劣势
  • 当修改对象的名字时,使用里面的方法,可能达不到想要的效果
  • 当对象赋值给其他变量时,其他变量使用,可能达不到想要的结果
var foo = {
    name: 'WEI'
}
foo.running = info.running;
foo.running(); //John在跑步

this在全局环境下的指向

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

  • 全局作用下:严格模式 undefined,非严格模式 window

console.log(this) //window

"use strict";
console.log(this); //undefined
  • node环境下为 {}
console.log(this); //{}

同一个函数中this不同的调用,导致不同的指向

function foo(){
    console.log(this)
}
//1. 直接调用这函数
foo()
// 2 创建一个对象, 对象中的函数指向foo
var obj = {
    name: 'wei',
    foo: foo
}
obj.foo()
//3 apply 调用
foo.apply('abc')

1.png

this中有四种绑定规则:
  • 默认绑定
  • 隐式绑定
  • 显示绑定
  • new绑定

默认绑定

什么是默认绑定?

默认绑定是 this 绑定规则中优先级最低的一种。它适用于独立函数调用的场景,也就是一个函数被直接调用,而不是作为对象的方法、不是通过 new 关键字、也不是通过 call(), apply(), 或 bind() 来调用的情况。

this 指向什么?

在默认绑定规则下,this 的指向取决于函数所在的执行环境是严格模式(Strict Mode)还是非严格模式(Non-strict Mode)

  1. 非严格模式 (Non-strict Mode):

    • 在这种模式下,默认绑定的 this 会指向全局对象
    • 在浏览器环境中,全局对象通常是 window。
    • 在 Node.js 环境中,全局对象是 global。
    • 潜在风险:  这可能导致意外地修改全局对象的属性,因为函数内部对 this 的操作会直接作用于全局对象。
  2. 严格模式 (Strict Mode):

    • 在这种模式下(通过在脚本或函数开头使用 'use strict'; 声明),默认绑定的 this 会是 undefined
    • 优点:  这是一种更安全、更可预测的行为。它防止了函数在无意中污染全局命名空间。如果你在严格模式下尝试访问 this 上的属性(而 this 是 undefined),会立即抛出一个 TypeError,帮助你更快地发现错误。

代码演示

案例 1
// 非严格模式 (默认)
var a = 10; // 在全局作用域声明变量,相当于 window.a = 10

function foo() {
  console.log(this);       // 输出: Window {...} (浏览器) 或 global {...} (Node.js)
  console.log(this.a);   // 输出: 10
  this.b = 20;           // 在全局对象上创建属性 b
}

foo(); // <-- 独立函数调用,应用默认绑定

console.log(window.b); // 输出: 20 (浏览器环境)
// console.log(global.b); // 输出: 20 (Node.js 环境)
console.log(b);        // 输出: 20 (因为 b 现在是全局变量)
案例 2
'use strict'; // 启用严格模式

var a = 10; // 全局变量

function bar() {
  console.log(this); // 输出: undefined
  // console.log(this.a); // TypeError: Cannot read properties of undefined (reading 'a')
  // this.c = 30;       // TypeError: Cannot set properties of undefined (setting 'c')
}

bar(); // <-- 独立函数调用,应用默认绑定

// 尝试访问全局 c 会报错 (因为 bar 函数内部的赋值失败了)
// console.log(c); // ReferenceError: c is not defined
案例 3
var x = 5;

function baz() {
  'use strict'; // 只在函数内部启用严格模式
  console.log(this); // 输出: undefined
  // console.log(this.x); // TypeError
}

function qux() {
  // 非严格模式
  console.log(this); // 输出: Window / global
  console.log(this.x); // 输出: 5
}

baz();
qux();
案例 4
var obj = {
    name: 'wei',
    foo: function () {
        console.log(this)
    }
}
obj.foo(); //{ name: 'wei', foo: [Function: foo] }
var bar = obj.foo;
bar() //window
案例 4
function foo() {
    console.log(this)
}
var obj = {
    name: 'wei',
    foo: foo
}
var bar = obj.foo
bar() //window
案例 5
function foo() {
    function bar() {
        console.log(this);
    }
    return bar;
}
var fun = foo()
fun() //window

隐式绑定

什么是隐式绑定?

隐式绑定是 this 绑定规则中最常见的一种。它发生在函数作为对象的方法被调用时。换句话说,当函数调用前面有一个“点” (.) 或方括号 ([]) 来访问对象的属性(而这个属性恰好是一个函数)时,隐式绑定规则就会生效。

this 指向什么?

在隐式绑定规则下,this 会被绑定到调用该方法的那个对象。也就是“点”或方括号左边的那个对象。这个对象被称为调用的上下文对象(Context Object)

触发条件:

  1. 函数是通过对象的属性访问(. 或 [])来调用的。
  2. 关键: 必须是直接调用。如果先把方法赋给一个变量,然后再调用这个变量,就不再是隐式绑定了(这会导致“隐式丢失”,通常会退回到默认绑定)。

示例:

1. 基本示例

 const person = {
  name: 'Alice',
  age: 30,
  greet: function() {
    // 在 greet 方法内部,this 指向调用它的对象 (person)
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
    console.log(this === person); // true
  }
};

person.greet(); // <-- 调用 greet 方法,点左边是 person 对象,应用隐式绑定
// 输出:
// Hello, my name is Alice and I am 30 years old.
// true
    
function foo() {
        console.log(this);
}
var obj = {
    name: 'wei',
    foo: foo
}
obj.foo() //obj对象: {name: 'wei', foo: ƒ}

2. 嵌套对象示例

this 指向的是直接调用该方法的对象,即使对象是嵌套的。

const company = {
  name: 'TechCorp',
  address: '123 Main St',
  department: {
    name: 'Engineering',
    manager: {
      name: 'Bob',
      report: function() {
        // 在 report 方法内部,this 指向调用它的对象 (manager)
        console.log(`Report from ${this.name} in the ${this.departmentName} department.`); // 注意:这里访问 this.departmentName 会失败
        // 要访问上层,需要明确引用
        console.log(`Correct: Report from ${this.name} in the ${company.department.name} department.`);
        console.log(this === company.department.manager); // true
      }
    }
  }
};

company.department.manager.report(); // <-- 调用 report 方法,点左边是 manager 对象,应用隐式绑定
// 输出:
// Report from Bob in the undefined department.
// Correct: Report from Bob in the Engineering department.
// true
    
var obj1 = {
    name: 'obj1',
    foo: function() {
        console.log(this);
    }
}
var obj2 = {
    name: 'obj2',
    bar: obj1.foo
}
obj2.bar()   //{name: 'obj2', bar: ƒ}

隐式丢失 (Implicitly Lost) - 最需要注意的情况!

当隐式绑定的函数(对象的方法)丢失了它的上下文对象时,this 的绑定就会改变,通常会退回到默认绑定(指向全局对象或 undefined)。

常见导致隐式丢失的场景:

  • 将方法赋给变量:

    const user = {
      name: 'Charlie',
      sayHi: function() {
        console.log(`Hi, I'm ${this.name}`); // 期望 this 是 user
        console.log(this);
      }
    };
    
    user.sayHi(); // 正常隐式绑定,输出: Hi, I'm Charlie, { name: 'Charlie', sayHi: [Function: sayHi] }
    
    console.log("--- Separator ---");
    
    const greetingFunc = user.sayHi; // 将方法赋给一个新变量,丢失了 user 这个上下文
    
     greetingFunc(); // <-- 独立函数调用,应用默认绑定
    
    // 非严格模式下:
    // 输出: Hi, I'm undefined (或全局对象的 name 属性值)
    // 输出: Window {...} 或 global {...}
    
    // 严格模式下 ('use strict'):
    // TypeError: Cannot read properties of undefined (reading 'name')
    // 因为 this 是 undefined
        
    
  • 将方法作为回调函数传递:

    const counter = {
      count: 0,
      increment: function() {
        // 'use strict'; // 在严格模式下更容易发现问题
        this.count++;
        console.log(this.count);
        console.log(this);
      }
    };
    
    // 直接调用,隐式绑定,正常工作
    // counter.increment(); // 输出 1, { count: 1, increment: f }
    
    // 作为回调函数传递给 setTimeout
    // setTimeout(counter.increment, 100); // 100ms 后执行
    
    // 当 setTimeout 执行回调时,它执行的是 "increment" 这个函数本身,
    // 而不是作为 counter 的方法来执行。上下文丢失!
    
    // 非严格模式下:
    // 100ms 后输出: NaN (因为 this 指向 window/global, this.count 是 undefined, undefined++ 是 NaN)
    // 100ms 后输出: Window {...} 或 global {...} (全局对象上多了一个 NaN 属性 count)
    
    // 严格模式下 ('use strict'):
    // 100ms 后抛出: TypeError: Cannot read properties of undefined (reading 'count')
        
    

如何解决隐式丢失?

通常使用 bind(), call(), apply() 或箭头函数来显式地绑定 this。

// 解决方法示例 (针对回调)
const counter = {
  count: 0,
  increment: function() {
    this.count++;
    console.log(this.count);
  }
};

// 1. 使用 bind()
const boundIncrement = counter.increment.bind(counter); // 创建一个新函数,this 永久绑定到 counter
setTimeout(boundIncrement, 100); // 100ms 后输出 1

// 2. 使用箭头函数 (常用)
setTimeout(() => {
  counter.increment(); // 在箭头函数内部调用,此时 increment 是作为 counter 的方法调用的,应用隐式绑定
}, 200); // 200ms 后输出 2 (假设之前 bind 的也执行了)

// 3. 在一些接受上下文参数的函数中使用 (例如数组方法)
// [1, 2].forEach(counter.increment, counter); // 不适用于 setTimeout
    

显示绑定

与隐式绑定(根据调用位置的对象自动确定 this)和默认绑定(独立调用时指向全局对象或 undefined)不同,显式绑定允许你明确地、强制地指定一个函数在调用时其内部 this 关键字应该指向哪个对象。

如何实现显式绑定?

JavaScript 提供了三个内置的函数方法来实现显式绑定:

  1. call()
  2. apply()
  3. bind()

这三个方法都存在于所有函数的 prototype 上(Function.prototype),因此任何函数都可以调用它们。

1. Function.prototype.call(thisArg, arg1, arg2, ...)

  • 作用: 立即调用该函数,并将函数内部的 this 绑定到你指定的第一个参数 thisArg 上。从第二个参数开始,可以依次传入函数执行所需的参数列表。

  • thisArg: 你希望函数执行时 this 指向的对象。

    • 如果传入 null 或 undefined,在非严格模式下 this 会指向全局对象 (window/global),在严格模式下 this 会是 undefined。
    • 如果传入原始值(数字、字符串、布尔值),this 会指向该原始值的包装对象 (Number, String, Boolean)。
  • arg1, arg2, ...: 传递给被调用函数的参数,需要逐个列出

示例:

function greet(punctuation, suffix) {
  console.log(`Hello, ${this.name}${punctuation} ${suffix || ''}`);
  console.log('this inside greet:', this);
}

const person1 = { name: 'Alice' };
const person2 = { name: 'Bob' };

// 使用 call() 将 greet 函数的 this 绑定到 person1
greet.call(person1, '!', 'Nice to meet you.');
// 输出:
// Hello, Alice! Nice to meet you.
// this inside greet: { name: 'Alice' }

// 使用 call() 将 greet 函数的 this 绑定到 person2
greet.call(person2, '?');
// 输出:
// Hello, Bob?
// this inside greet: { name: 'Bob' }

// 演示 thisArg 为 null (非严格模式)
function showGlobalName() {
  console.log(this.name); // 假设全局有 var name = 'Global';
}
// var name = 'Global'; // 在浏览器或 Node 全局定义
// showGlobalName.call(null); // 在非严格模式下会输出 "Global"

// 演示 thisArg 为原始值
function showType() {
  console.log(typeof this, this instanceof String, this.valueOf());
}
showType.call("hello"); // 输出: object true hello
    

2. Function.prototype.apply(thisArg, [argsArray])

  • 作用: 与 call() 非常相似,也是立即调用函数并将 this 绑定到 thisArg。
  • 主要区别: apply() 接受一个数组(或类数组对象)作为第二个参数,数组中的元素会作为参数传递给被调用的函数。
  • thisArg: 同 call()。
  • [argsArray]: 一个数组或类数组对象,其元素将作为参数传递给函数。如果不需要传递参数,可以传入 null 或 undefined。

示例:

function introduce(city, country) {
  console.log(`I am ${this.name} from ${city}, ${country}.`);
  console.log('this inside introduce:', this);
}

const person3 = { name: 'Charlie' };
const locationArgs = ['London', 'UK'];

// 使用 apply() 将 introduce 函数的 this 绑定到 person3,并传递参数数组
introduce.apply(person3, locationArgs);
// 输出:
// I am Charlie from London, UK.
// this inside introduce: { name: 'Charlie' }

// apply() 的常见用途:调用需要参数列表的函数,但你只有一个数组
const numbers = [5, 2, 8, 1, 9];
// Math.max 需要多个数字参数,而不是数组
// const maxNum = Math.max(numbers); // 错误!会返回 NaN
const maxNum = Math.max.apply(null, numbers); // 使用 apply 将数组元素展开作为参数
console.log(maxNum); // 输出: 9
// ES6 Spread 操作符提供了更简洁的方式: Math.max(...numbers)
    

3. Function.prototype.bind(thisArg, arg1, arg2, ...)

  • 作用: bind() 与 call() 和 apply() 不同,它不会立即执行函数。相反,它会创建一个新的函数(称为绑定函数 (Bound Function) ),这个新函数的 this 值被永久地绑定到 bind() 的第一个参数 thisArg。
  • 永久绑定 (Hard Binding): 一旦使用 bind() 创建了绑定函数,之后无论如何调用这个绑定函数(即使使用 call() 或 apply() 再次尝试改变它的 this),其内部的 this 始终指向 bind() 时设定的 thisArg。
  • 参数预设 (Partial Application): bind() 从第二个参数开始,还可以预先设定函数调用时所需的部分或全部参数。当调用绑定函数时,传递给绑定函数的参数会追加到预设参数之后。
  • 返回值: 一个新的、this 和部分参数已绑定的函数。

示例:

function foo() {
    console.log(this);
}

// foo.call('aaa')
// foo.call('aaa')
// foo.call('aaa')
// foo.call('aaa')
//都是String {'aaa'},但是太麻烦了

//默认当地和显示绑定bind冲突,显示绑定优先级大
var newFoo = foo.bind('aaa')

newFoo()
function logDetails(level, message) {
  console.log(`[${level.toUpperCase()}] ${this.source}: ${message}`);
}

const loggerConfig = { source: 'WebServer' };
const networkConfig = { source: 'NetworkMonitor' };

// 创建一个 this 绑定到 loggerConfig 的新函数
const webLogger = logDetails.bind(loggerConfig);
// 创建一个 this 绑定到 networkConfig 且第一个参数预设为 'INFO' 的新函数
const networkInfoLogger = logDetails.bind(networkConfig, 'info');

// 调用绑定函数
webLogger('ERROR', 'Failed to process request.');
// 输出: [ERROR] WebServer: Failed to process request.

networkInfoLogger('Connection established.'); // 只需要传递剩余的 message 参数
// 输出: [INFO] NetworkMonitor: Connection established.

// 演示 bind 的永久性
const anotherContext = { source: 'AnotherSource' };
webLogger.call(anotherContext, 'WARN', 'Trying to change context?');
// 输出仍然是: [WARN] WebServer: Trying to change context?
// 'this' 仍然指向 loggerConfig,而不是 anotherContext
    

为什么要使用显式绑定?

  • 解决 this 丢失问题: 特别是在回调函数或将方法赋值给变量时,使用 bind() 可以确保函数在稍后执行时 this 指向正确的对象。

  • 借用方法: 可以让一个对象“借用”另一个对象的方法,并在自己的上下文(this 指向自己)中执行。例如,类数组对象(如 arguments 或 DOM NodeList)借用 Array.prototype 的方法。

    function exampleFunc() {
      // arguments 不是真数组,没有 slice 方法
      // const argsArray = arguments.slice(1); // TypeError
    
      // 使用 call() 借用 Array.prototype.slice
      const argsArray = Array.prototype.slice.call(arguments, 1);
      console.log(argsArray);
    }
    exampleFunc('a', 'b', 'c'); // 输出: [ 'b', 'c' ]
        
    
  • 创建柯里化 (Currying) 或部分应用 (Partial Application) 的函数: bind() 可以方便地创建预设了部分参数的新函数。

优先级:

显式绑定 (call, apply, bind) 的优先级高于隐式绑定和默认绑定。如果一个函数同时满足多种绑定条件,显式绑定会胜出。(注意:new 绑定的优先级又高于显式绑定)。

总结:

  • call() 和 apply() 立即执行函数,允许你一次性地指定 this 和参数。call 接收单个参数列表,apply 接收参数数组
  • bind() 不立即执行,而是返回一个新函数,这个新函数的 this 被永久绑定,并且可以预设参数。
  • 显式绑定是控制函数执行上下文 (this) 的强大工具,特别适用于回调、方法借用和函数式编程模式。

new绑定

这是 this 绑定规则中非常特殊且优先级很高的一种。它发生在使用 new 操作符来调用一个函数时。这个被 new 调用的函数通常被称为构造函数(Constructor Function)

new 操作符做了什么?

当你使用 new SomeFunction(...) 时,JavaScript 引擎会执行以下四个步骤:

  1. 创建新对象: 一个全新的、空的原生 JavaScript 对象被创建。

  2. 设置原型链接: 这个新创建的对象的内部 [[Prototype]](即 proto)属性被设置为指向构造函数(SomeFunction)的 prototype 对象。这步建立了原型链,使得新对象可以继承构造函数原型上的属性和方法。

  3. 绑定 this 并执行函数: 构造函数(SomeFunction)被调用,并且其内部的 this 关键字被绑定(指向)到第一步创建的那个新对象上。 这就是 new 绑定的核心。构造函数内部的代码通常会使用 this 来给新对象添加属性和方法。

  4. 返回新对象(通常情况下):

    • 如果构造函数没有显式地使用 return 语句返回一个对象(或者返回的是 null, undefined 或其他原始类型值),那么 new 表达式的结果就是第一步创建并经过第三步初始化的那个新对象
    • 如果构造函数显式地 return 了另一个对象,那么 new 表达式的结果就是这个被 return 的对象,而不是第一步创建的新对象。

this 指向什么?

在 new 绑定规则下,构造函数内部的 this 总是指向在第一步中新创建的那个对象实例

示例:

 function Car(make, model, year) {
  // 在 new 调用时,这里的 'this' 指向新创建的 Car 对象实例
  console.log('Inside constructor, this:', this);

  // 使用 this 给新对象添加属性
  this.make = make;
  this.model = model;
  this.year = year;
  this.isRunning = false;

  // 不推荐在构造函数中直接添加方法(最好放 prototype)
  // this.start = function() { this.isRunning = true; }

  // 默认情况下,隐式返回 this (新创建的对象)
  // 如果 return { custom: 'object' }; 则 new 的结果是这个 custom object
  // 如果 return 5; (原始值),则 return 语句被忽略,仍然返回 this
}

// 使用 new 调用 Car 构造函数
const myCar = new Car('Toyota', 'Camry', 2023);

// myCar 是由 new Car() 创建的对象实例
console.log('myCar instance:', myCar);
console.log(myCar.make);    // 输出: Toyota
console.log(myCar.year);    // 输出: 2023

// 验证原型链是否设置正确
console.log(Object.getPrototypeOf(myCar) === Car.prototype); // 输出: true
    

与非 new 调用的对比:

如果同一个函数不使用 new 调用,this 的指向将遵循其他规则(通常是默认绑定):

function Gadget(name) {
  // 'use strict'; // 在严格模式下,下面的调用会报错,因为 this 是 undefined
  this.name = name;
  console.log('Inside Gadget, this:', this);
}

// 使用 new 调用 (new 绑定)
const gadget1 = new Gadget('Phone');
console.log('Gadget 1:', gadget1); // 输出: Gadget 1: Gadget { name: 'Phone' }

console.log("--- Separator ---");

// 不使用 new 调用 (默认绑定)
const gadget2Result = Gadget('Watch');
// 在非严格模式下:
// Inside Gadget, this: Window {...} 或 global {...}
// 全局对象上被添加了 name 属性: window.name = 'Watch'
console.log('Gadget 2 Result:', gadget2Result); // 输出: Gadget 2 Result: undefined (因为函数默认返回 undefined)
console.log(window.name); // 输出: Watch (浏览器环境)
    

优先级:

new 绑定的优先级非常高,仅次于箭头函数(箭头函数没有自己的 this 绑定)。

  • new 绑定 > 显式绑定 (bind, call, apply) > 隐式绑定 (方法调用) > 默认绑定

这意味着,即使你尝试对一个函数使用 bind, call 或 apply 来设置 this,然后再使用 new 来调用它,new 绑定仍然会生效,this 还是会指向新创建的对象。

function Creature(type) {
  this.type = type;
  console.log(`Creating a ${this.type}, this is:`, this);
}

const predefinedContext = { type: 'SHOULD BE IGNORED' };

// 尝试用 bind 预设 this,但随后用 new 调用
const BoundCreature = Creature.bind(predefinedContext);
const creatureInstance = new BoundCreature('Dragon');
// 输出: Creating a Dragon, this is: Creature { type: 'Dragon' }
// 'this' 指向新创建的 creatureInstance,而不是 predefinedContext

console.log(creatureInstance.type); // 输出: Dragon
    

关键点总结:

  • 触发条件: 使用 new 操作符调用函数。
  • this 指向: 函数内部的 this 指向新创建的对象实例。
  • 目的: 主要用于面向对象编程中的对象实例化和初始化。
  • 高优先级: new 绑定的优先级高于显式绑定、隐式绑定和默认绑定。
  • 箭头函数例外: 不能对箭头函数使用 new 操作符,因为箭头函数没有自己的 this,也缺少构造函数所需的内部机制。尝试这样做会抛出 TypeError。