深入JavaScript之this

前言

在学习JavaScript的过程中,this是一个不可忽视的概念,很多巧妙的操作都是基于this实现的。但this在不同环境下的取值往往是一个令人头疼的问题。

this是如何定义的

先看一下官方文档中对this的说明。

ECMAScript标准中this的解释


The abstract operation ResolveThisBinding determines the binding of the keyword this using the LexicalEnvironment of the running execution context

大致意思是,this关键字是一个运行时的语义,指向活跃状态的执行上下文的词法环境(LexicalEnvironment)

MDN中this的定义

当前执行上下文(global、function 或 eval)的一个属性,在非严格模式下,总是指向一个对象,在严格模式下可以是任意值。

小结

上面的两种关于this的解释都和上下文有关,在JavaScript中,一段代码在执行前 会生成一个活动对象,作为代码的执行环境,里面存储了代码段中声明的变量,函数等,我们也将活动对象称做执行上下文。

除了变量,函数外,在执行上下文中还会确定this的值,也就是this绑定。在this绑定过程中,this一般指向代码运行时所在的环境(LexicalEnvironment)。代码执行过程中获取的this都是从执行上下文中取值的。

this有什么作用

万物皆有因,在JavaScript这门编程语言中this被创建出来一定有存在的意义,能够发挥一定的作用。
this设计的目的就是在函数体内部,指代当前函数的运行环境。因此我们可以通过this在函数体内部便捷的引用运行环境中的变量而不需要传参,极大的方便了开发。

我们通过一个简单的例子来体验一下this的效果。

javascript 复制代码
var person = {
    name: '小明',
    age: 18,
    hobby: '篮球',
    playGame: function(){
      // 待补充
    }
}

现在有一个对象person,有name,age,hobby三个属性,还有一个playGame方法,现在我们的需求是:

调用person.playGame()方法,输出:我叫小明,今年18岁,我的爱好是篮球!

如果没有this,我们该如何实现这个操作?直接的思路就是传入需要的数据,然后输出。

javascript 复制代码
var person = {
    name: '小明',
    age: 18,
    hobby: '篮球',
    playGame: function(name,age,hobby){
      console.log(`我叫${name},今年${age}岁,我的爱好是${hobby}!`)
    }
}
person.playGame(person.name,person.age,person.hobby);

再简化一下,直接传入person对象,得到的代码为:

javascript 复制代码
var person = {
    name: '小明',
    age: 18,
    hobby: '篮球',
    playGame: function(self){
      console.log(`我叫${self.name},今年${self.age}岁,我的爱好是${self.hobby}!`)
    }
}
person.playGame(person);

如果是使用this来实现这个需求,则代码为:

javascript 复制代码
var person = {
    name: '小明',
    age: 18,
    hobby: '篮球',
    playGame: function(){
      console.log(`我叫${this.name},今年${this.age}岁,我的爱好是${this.hobby}!`)
    }
}
person.playGame();

两者对比发现,区别不是很大,只不过person是显示传参,需要我们手动设置,存储在函数的参数列表里面

而this是隐式的,JavsScript直接帮我们设置了,存储在代码执行前生成的上下文中。通过this,我们可以在函数内部获取当前函数的运行环境,使用环境中的一些变量。这也是this最初被设计的目的。

再看一个经典的例子。

javascript 复制代码
var f = function () {
  console.log(this.x);
}

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// 单独执行
f() // 1

// obj 环境执行
obj.f() // 2

同一个函数,在不同的环境中执行,它的this指向是不同的,它总是指向当前函数的运行环境

判断this的指向

this一般在函数中使用,但是在不同的函数中,this指向也是有所区别的。

标准函数

在标准函数中,this引用的是把函数当成方法调用的上下文对象,这时通常称其为this值。

在全局上下文中调用函数时,this指向window

javascript 复制代码
var foo = 123;
function print(){
	this.foo = 234;
    console.log(this); // window
	console.log(foo); // 234
}
print();	//等价于 window.print()

在全局上下文中调用,等价于window对象在调用这个函数,此时函数的this指向的是window对象。

以某个对象的方法调用时,this指向这个对象

javascript 复制代码
var color = 'red';
let o = {
    color: 'blue',
};

function sayColor() {
    console.log(this.color);
}

sayColor();   //'red'

o.sayColor = sayColor;
o.sayColor();  //'blue'

小结

其实上面两种情况,都可以看做是同一种情况,即把函数当做方法调用 。只不过在全局调用时,window对象是可以省略的。

从上面的例子可以看出,同一个函数在不同的地方调用,其this的指向是不同的 。也就是说,this的指向是在函数调用时 确定的,和函数在哪个作用域创建无关

做几道例题测试下吧

1,let,const
javascript 复制代码
let a = 1;
const b = 2;
var c = 3;
function print() {
    console.log(this.a);  //undefined
    console.log(this.b);  //undefined
    console.log(this.c);  //3
}
print();
console.log(this.a);  //undefined

let/const定义的变量存在暂时性死区,而且不会挂载到window对象上,因此print中是无法获取到a和b的。

2,对象内执行
javascript 复制代码
var a = 1;
function foo() {
    console.log(this.a); 
}
const obj = {
    a: 10,
    bar() {
        foo(); // 1
    }
}
obj.bar(); 

foo虽然在obj的bar函数中,但foo函数仍然是独立运行的,foo中的this依旧指向window对象。

3,函数内执行
javascript 复制代码
var a = 1
function outer() {
    var a = 2
    function inner() {
        console.log(this.a) // 1
        console.log(a);   //2
    }
    inner()
}
outer()

查找标准函数中某个变量的值时,可以通过作用域链寻找,但寻找标准函数中的this值时,需要判断它被哪个对象所调用。inner()正常执行,不是作为对象的某个方法调用,this指向Window

4,自执行函数
javascript 复制代码
var a = 1;
(function(){
    console.log(this);
    console.log(this.a)
}())
function bar() {
    b = 2;
    (function(){
        console.log(this);
        console.log(this.b)
    }())
}
bar();

默认情况下,自执行函数的this指向window 自执行函数只要执行到就会运行,并且只会运行一次,this指向window。

箭头函数

箭头函数的this

箭头函数没有this,它的this指向的是定义箭头函数的作用域中对应的上下文

javascript 复制代码
var color = 'red';
let o = {
    color: 'blue',
};

let sayColor = () => console.log(this.color)

sayColor();   //'red'

o.sayColor = sayColor;
o.sayColor();  //'red'

上述代码中,箭头函数是在全局作用域中定义的,也就是说this会指向全局作用域。由于JS采用的是静态作用域,作用域 是在代码创建时 就确定了的,所以,箭头函数中的this是指向定义箭头函数的作用域中执行上下文的。

如果是在全局中定义箭头函数 ,箭头函数中的this指向全局上下文,全局上下文中的this是指向window。

如果是在标准函数中定义箭头函数 ,那么箭头函数的this指向标准函数上下文中的this。标准函数的this是根据函数执行的位置确定的

做几道例题测试下

1 在对象方法中使用箭头函数
javascript 复制代码
var name = 'tom'
const obj = {
    name: 'zc',
    intro: () => {
        console.log('My name is ' + this.name)
    }
}
obj.intro()  //My name is tom

箭头函数在全局作用域中创建,this指向全局上下文。

2 箭头函数与普通函数比较
javascript 复制代码
var name = 'tom';
const obj = {
    name: 'zc',
    intro: function () {
        return () => {
            console.log('My name is ' + this.name)
        }
    },
    intro2: function () {
        return function () {
            console.log('My name is ' + this.name)
        }
    }
}
obj.intro2()()  //My name is tom
obj.intro()()   //My name is zc

分析:调用intro方法返回值是一个箭头函数,箭头函数中的this是取定义箭头函数作用域中的this,也就是intro函数的作用域。intro函数是一个标准函数,它的this根据调用位置确定,obj调用的intro方法,所以intro中的this指向obj。obj.intro()() //My name is zc

intro2方法返回值是一个标准函数,它的this是根据调用位置来确定的。其在全局中调用,所以this指向window

javascript 复制代码
obj.intro2()()  //My name is tom

//等价于
let fn = obj.intro2();  
fn()   //My name is tom    全局中调用

new 构造函数

构造函数中this的指向

如果函数是使用new调用,函数执行前会新创建一个对象,this指向这个创建的对象。

javascript 复制代码
function User(name, age) {
    this.name = name;
    this.age = age;
}
var name = 'Tom';
var age = 18;

var zc = new User('zc', 24);
console.log(zc.name) // 'zc'

new 操作符实现的操作

  1. 创建一个对象。
  2. 将对象的原型对象设置为构造函数的原型对象,来继承原型对象上的属性和方法。
  3. 将构造函数中的this指向该对象,为这个对象添加属性和方法。
  4. 返回这个对象。

new 操作符手写实现

javascript 复制代码
function createNew(con,...args){
  //以构造函数原型对象为原型对象创建一个对象
  let obj = Object.create(con.prototype);
  //将构造函数中的this指向这个对象
  const result = con.call(obj,...args);
  // 返回创建的对象
  return result instanceof Object? result:obj;
}

//测试
function Person(name,age){
  this.name = name;
  this.age = age;
}

let person1 = new Person('张三',25);
console.log(person1);
let person2 = createNew(Person,'李四',26);
console.log(person2);

控制台输出结果:

更改this的指向

函数中this的值是JavaScript代码在执行上下文中自动设置的,但JavaScript也提供了callapplybind等方法让我们手动设置this的值。

call方法

方法说明

call方法使用一个指定的this值 和单独给出的一个或多个参数调用一个函数 。返回调用函数的返回值。

它将调用函数中this指向改成指向call方法中传入的第一个参数。

语法格式:function.call(thisArg, arg1, arg2, ...)

示例:

javascript 复制代码
var name = "honny";

function sayHello(name,emotion){
  console.log(`hello ${this.name},My name is ${name},I'm very ${emotion} to see you`);
}

//全局调用sayHello方法,this指向window
sayHello('Jock','happy');
//hello honny,My name is Jock,I'm very happy to see you

let person = {
  name:'nack'
}

//使用call方法,将sayHello函数中的this指向person
sayHello.call(person,'Jock','happy');
//hello nack,My name is Jock,I'm very happy to see you

手写call方法

javascript 复制代码
//手写call方法
function myCall(target,...args){
    //获取目标对象
    let obj = target||window;
    //创建唯一标识
    let symbolName = Symbol();
  
    //获取方法:此时是需要执行的函数调用call方法,this指向执行的函数
    //function.call()  this->function
    //在目标对象上添加这个函数
    obj[symbolName] = this;
  
    //执行目标对象上添加的函数 并获取返回值
    //此次调用中,函数是在目标对象中调用的,所以函数中的this指向目标对象
    let result = obj[symbolName](...args);
  
    //调用完毕后删除目标对象上添加的函数
    delete obj[symbolName];
  
    //返回函数调用结果
    return result;
}

//测试
var name = "honny";

function sayHello(name,emotion){
  console.log(`hello ${this.name},My name is ${name},I'm very ${emotion} to see you`);
}

let person = {
  name:'nack'
}

//在函数原型上添加myCall方法
Function.prototype.myCall = myCall;
//调用myCall方法
sayHello.myCall(person,'Jock','happy');
// hello nack,My name is Jock,I'm very happy to see you

apply方法

方法说明

apply调用一个具有给定值this的函数,以及以一个数组或类数组对象的形式提供的参数,返回调用函数的返回值。同call方法类似,它也能够将调用函数中的this的指向改变为apply方法传入的第一个参数。不过于call方法不同的是,call方法接收一个参数列表,但apply方法接收一个单数组。

语法格式:function.apply(thisArg,[...arg])

示例:

javascript 复制代码
var name = "honny";

function sayHello(name,emotion){
  console.log(`hello ${this.name},My name is ${name},I'm very ${emotion} to see you`);
}

sayHello('Jock','happy');
//hello honny,My name is Jock,I'm very happy to see you

let person = {
  name:'nack'
}
sayHello.apply(person,['Jock','happy']);
//hello nack,My name is Jock,I'm very happy to see you

注意:apply以数组的方式接收调用函数的参数,但在调用函数中还是以参数列表的形式接收的

手写apply方法

javascript 复制代码
//手写apply
function myApply(target,argsArray){
  //1 获取目标对象
  let obj = target || window;
  //2 创建唯一编码
  let symbol = Symbol();
  //3 在目标对象上添加函数
  obj[symbol] = this;
  //4 执行函数获取结果
  let result = obj[symbol](...argsArray);
  //5 执行完毕,删除函数
  delete obj[symbol];
  //6 返回执行结果
  return result;
}

//测试
var name = "honny";
function sayHello(name,emotion){
  console.log(`hello ${this.name},My name is ${name},I'm very ${emotion} to see you`);
}
let person = {
  name:'nack'
}
Function.prototype.myApply = myApply;
sayHello.myApply(person,['Jock','happy']);
//hello nack,My name is Jock,I'm very happy to see you

bind方法

方法说明

bind方法创建一个新的函数,在bind被调用时,这个新函数的this值被指定为bind方法的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

与call和apply相同的是,bind方法能够将调用函数的this指向bind的第一个参数。但不同的是bind并不会执行这个函数,而是返回更改this后的新函数

语法格式:function.bind(thisArg, arg1, arg2, ...)

示例:

javascript 复制代码
var name = "honny";

function sayHello(name,emotion){
  console.log(`hello ${this.name},My name is ${name},I'm very ${emotion} to see you`);
}

sayHello('Jock','happy');
//hello honny,My name is Jock,I'm very happy to see you

let person = {
  name:'nack'
}

//情况一
let sayHelloAfterBind = sayHello.bind(person,'Jock','happy');
sayHelloAfterBind();
//hello nack,My name is Jock,I'm very happy to see you

//情况二
let sayHelloAfterBind = sayHello.bind(person);
sayHelloAfterBind('Jock','happy');
//hello nack,My name is Jock,I'm very happy to see you

//情况三
let sayHelloAfterBind = sayHello.bind(person,'Jock');
sayHelloAfterBind('happy');
//执行方法时接收的实际参数为:'Jock','happy'
//hello nack,My name is Jock,I'm very happy to see you

注意:由于调用bind返回的是一个函数,函数是可以传参数的,函数执行使用的参数是bind第一个参数之后的参数加上调用函数时传入的参数

手写bind方法

javascript 复制代码
//手写 bind
function myBind(target,...args1){
  //1 获取目标对象
  let obj = target || window;
  //2 创建唯一编码
  const symbol = Symbol();
  //3 再目标对象上添加函数
  obj[symbol] = this;
  //4 返回一个函数
  return function(...args2){
    return obj[symbol](...args1,...args2);
  }
}

//测试
var name = "honny";

function sayHello(name,emotion){
  console.log(`hello ${this.name},My name is ${name},I'm very ${emotion} to see you`);
}

let person = {
  name:'nack'
}

Function.prototype.myBind = myBind;
    
let sayHelloMyBind = sayHello.myBind(person,'Jock');
sayHelloMyBind('happy');
//hello nack,My name is Jock,I'm very happy to see you

参考资料

JavaScript 的 this 原理 - 阮一峰的网络日志 (ruanyifeng.com)
JS 里为什么会有 this - 知乎 (zhihu.com)
ECMAScript® 2019 Language Specification (ecma-international.org)
面试官为啥总是让我们手撕call、apply、bind? - 掘金 (juejin.cn)

相关推荐
恋猫de小郭5 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅12 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606113 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了13 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅13 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅13 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅13 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment14 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅14 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊14 小时前
jwt介绍
前端