带你真正了解 JavaScript 中的 this

this 的误解

  • this 默认指向函数自己。

任何情况下,this 都不会默认指向函数自己,除非使用 bind 绑定的方式修改 this 为函数自己。

  • this 指向函数作用域或上下文对象。

需要明确,任何情况下,this 都不默认指向函数的词法作用域或上下文对象,作用域或者说上下文对象确实与对象类似,可见的标识符都是其属性,但是该对象只存在于 js 引擎内部,无法在 js 环境下被访问。

this 是什么

本质上,作用域工作模型分两种,一种是词法作用域,一种是动态作用域。

  • 词法作用域:词法作用域指的是在词法阶段产生的作用域,由书写者在写代码时所写的变量及作用域的位置所决定。引擎根据这些位置信息来查找标识符即变量的位置。例如:无论函数在哪里、如何被调用,它的词法作用域都只由被声明时所处的位置决定。

  • 动态作用域:动态作用域是一个在运行时被动态确定的形式,而不是在静态时被确定。动态作用域不关心函数与作用域如何嵌套或何处声明,只关心它们在何处调用,也就是说。它的作用域链是基于调用栈而非作用域嵌套。例:

function foo() {	
  console.log(a);	
}	
function bar() {	
  var a = 3;	
  foo();	
}	
var a = 2;	
bar();

如果是词法作用域,根据作用域规则,最终打印为 2; 可是动态作用域会顺着调用栈去寻找变量,所以打印结果为 3。

js 的作用域规则属于词法作用域规则。

而 this 的机制与动态作用域的机制相近。this 在函数运行时绑定,不在编写时绑定,其上下文取决于调用时的条件。this 绑定与函数声明位置无关,取决于函数调用方式。

当一个函数被调用时,创建一个活动记录(也称执行上下文对象),此记录对象包含函数调用栈、调用方式、传入参数等信息,this 是这个记录的一个属性。

调用栈

调用栈,其实就是函数的调用链,而当前函数的调用位置就在调用栈的倒数第二个位置(浏览器开发者工具中,给某函数第一行打断点 debugger,运行时,可以展示调用列表 call stack) 。示例:

//全局作用域下	
function func(val) {	
  if (val <= 0) return;	
  console.log(val);	
  func(val - 1);	
}	
func(5);

执行栈用来存储运行时的执行环境。当然,栈遵循先进后出的规则。

上面代码的执行栈如下:执行创建时:创建全局执行环境 => func(5) => func(4) => func(3) => func(2) => func(1)。

执行完毕销毁时:func(1) => func(2) => func(3) => func(4) => func(5) => 创建全局执行环境。

this 的绑定规则

上面的可以完全不记,只要这部分牢记,就完全够用了

默认绑定

产生于独立函数调用时,可以理解为无法应用其他规则时的默认规则。默认绑定下的 this 在非严格模式的情况下,默认指向全局的 window 对象,而在严格模式的情况下,则指向 undefined。

ps1:以下规则,都是以函数环境为前提的,也就是说,this 是放在函数体内执行的。在非函数环境下,也就是浏览器的全局作用域下,不论是否严格模式,this 将一直指向 window。一个冷知识:浏览器环境下的全局对象是 window,其实除此之外还有一个特别的关键字,globalThis,在浏览器环境下打印该对象,指向 window。

ps2: this 所在的词法作用域在编写或声明时添加了"use strict",那么,运行时 this 指向 undefined,但是,如果 this 所在的函数作用域中并未添加"use strict",而运行或调用该函数的词法作用域里有添加,那么也不影响,依然指向 window。

ps3:对于 JS 代码中没有写执行主体的情况下,非严格模式默认都是 window 执行的,所以 this 指向的是 window,但是在严格模式下,若没有写执行主体,this 指向是 undefined;

隐式绑定

判断调用位置是否有上下文对象或者说是否有执行主体。简单说,一个对象调用了它所"拥有"的方法,那么,这个方法中的 this 将指向这个对象(对象属性引用链中只有上一层或者说最后一层才在调用位置中起作用,例:a.b.c.func(),func 中的 this 只会指向 c 对象)。

函数方法并不属于对象

说到对象与其包含的函数方法的关系,通常人们一提到方法,就会认为这个函数属于一个对象 ,这是一个误解,函数永远不会属于某个对象,尽管它是对象的方法。其中存在的关系只是引用关系。示例 1:

//在对象的属性上声明一个函数	
var obj = {	
  foo: function func() {}	
};

示例 2:

//独立声明一个函数然后用对象的属性引用	
function func() {}	
var obj = {	
  foo: func	
};

上述两个例子效果是一样的,没有任何本质上的区别,很明显,函数属于它被声明时所在的作用域;我们都知道函数本质上是被存储在堆内存中,而函数的引用地址被存放在栈内存中方便我们取用,那么实际上对象中的属性持有的只是存在栈内存里函数的地址引用。

如果非要把持有引用地址当成一种属于关系的话,一个函数的地址可以被无数变量引用持有,那么这所有的变量都算是拥有这个函数,然而,属于关系是唯一的,所以该观点并不成立。

隐式丢失,即间接引用

示例 1:

var b = {	
  func: function() {}	
};	
var a = b.func;	
a();

示例 2:

var b = {	
  func: function() {}	
};	
function foo(fn) {	
  fn();	
}	
foo(b.func);

这两种情况下,this 指向丢失(不指向对象),而原理在上面的”函数方法并不属于对象“里已经揭露,在这里,不论是 a 还是 fn(而参数传递其实就是一种隐式赋值,传入函数也是),拿到的都只是函数的引用地址。

我们修改下上面的两个示例就一目了然了。

示例 1:

function bar() {}	
var b = {	
  func: bar	
};	
var a = b.func; //相当于  var a=bar;	
a();

示例 2:

function bar() {}	
var b = {	
  func: bar	
};	
function foo(fn) {	
  fn();	
}	
foo(b.func); //相当于foo(bar);

显式绑定

隐式绑定中,方法执行时,对象内部包含一个指向函数的属性,通过这个属性间接引用函数,从而实现 this 绑定。

显式绑定也是如此,通过 call,apply 等方法,实现 this 的强制绑定(如果输入字符串、布尔、数字等类型变量当做 this 绑定对象,那么这些原始类型会被转为对象类型,如 new String,new Boolean,new Number,这种行为叫装箱)。绑定示例 1:

var a = 1;	
function func() {	
  console.log(this.a);	
}	
var obj = {	
  a: 0	
};	
func.apply(obj); //0

绑定示例 2:

var a = 1;	
function func() {	
  console.log(this.a);	
}	
var obj = {	
  a: 0	
};	
func.call(obj); //0

然而这依然无法解决可能丢失绑定的问题(比如处理回调函数,由于使用 call、apply 就会直接调用,而回调函数的调用无法人为介入控制所以回调函数上用不上 call、apply)。

示例代码:

var a = 1;	
function func() {	
  console.log(this.a);	
}	
var obj = {	
  a: 0	
};	
setTimeout(func.call(obj), 1000); //立即执行了,无法满足延迟执行的需求
显式绑定中的硬绑定

bind 是硬绑定,通过使用 bind 方法的硬绑定处理,将回调函数进行包装,而得到的新函数在被使用时不会丢失绑定(利用了柯理化技术,柯理化技术依托于闭包)。

示例:

var a = 1;	
function func() {	
  console.log(this.a);	
}	
var obj = {	
  a: 0	
};	
var newFunc = func.bind(obj);	
setTimeout(newFunc, 1000); //延迟1秒后打印0
显式绑定中的软绑定

硬绑定降低了函数的灵活性,无法再使用隐式绑定或显式绑定修改 this。

示例:

function func() {	
  console.log(this.a);	
}	
var obj = {	
  a: 0	
};	
var o = {	
  a: 2	
};	
var newFunc = func.bind(obj);	
newFunc.apply(o); //0

为了解决灵活性的问题,我们可以在硬绑定的原理基础上尝试 shim 一个新的绑定方式---软绑定。

示例:

Function.prototype.softBind = function(self) {	
  var func = this;	
  var oldArg = [...arguments].slice(1);	
  return function() {	
    var newArgs = oldArg.concat([...arguments]);	
    var _this = !this || this === window ? self : this;	
    func.apply(_this, newArgs);	
  };	
};	
function func() {	
  console.log(this.a);	
}	
var obj = {	
  a: 0	
};	
var o = {	
  a: 2	
};	
var newFunc = func.softBind(obj);	
newFunc(); //0	
newFunc.apply(o); //2

核心代码:

var _this = (!this || this === window)?self:this;	
//如果this绑定到全局或者undefined时,那么就保持包装函数softBind被调用时的绑定,否则修改this绑定到当前的新this。

ps:js 的许多内置函数都提供了可选参数,用来实现绑定上下文对象,例:数组的 forEach、map、filter 等方法,第一个参数为回调函数,第二个为将绑定的上下文对象。

new 绑定

传统语言中,构造函数是类中的一些特殊方法,使用 new 初始化类时会调用类中的构造函数。而 js 中的所谓"构造函数"其实只是普通的函数,它们不属于某个类,也不会实例化一个类。实际上 js 中并不存在构造函数,只有对于函数的构造调用。使用 new 调用函数(构造调用) 时,

  • 执行函数;

  • 创建一个全新对象(若未返回其他对象时,那么 new 表达式中的函数调用会自动返回这个新对象,若返回了其他对象,则 this 将绑定在返回的对象上);

  • 新对象会被执行原型连接;

  • 新对象会绑定到函数调用的 this。

优先级

new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定。

箭头函数 this 绑定

根据该函数所在词法作用域决定,简单来说,箭头函数中的 this 绑定继承于该函数所在作用域中 this 的绑定。

箭头函数没有自己的 this,所以使用 bind、apply、call 无法修改其 this 指向,其 this 依然指向声明时继承的 this。

虽然 bind 不能修改其 this 指向,但是依然可以实现预参数的效果;而 apply 与 call 的参数传递也是生效的。

ps:箭头函数不只没有自己 this,也没有 arguments 对象。

写在最后

需要声明的一点是,我不是一个教授者,我只是一个分享者、一个讨论者、一个学习者,有不同的意见或新的想法,提出来,我们一起研究。分享的同时,并不只是被分享者在学习进步,分享者亦是。

知识遍地,拾到了就是你的。

既然有用,不妨点个在看,让更多的人了解、学习并提升。

阅读完后三部曲

非常感谢各位花时间阅读完,衷心希望各位小伙伴可以花少量的时间帮忙做两件事:

  • 动动你的手指,帮忙点个在看吧,你的点赞是对我最大的动力。

  • 公众号回复1或者加群,进入前端答疑解惑群。

  • 希望各位关注一下我的公众号,新的文章第一时间发到公众号,公众号主要发一些个人随笔、读书笔记、还有一些技术热点和实时热点,并且还有非常吸引人的我个人自费抽奖活动哦~

640?wx_fmt=jpeg

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值