前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >带你真正了解 JavaScript 中的 this

带你真正了解 JavaScript 中的 this

作者头像
石燕平
发布2019-10-14 01:20:42
4450
发布2019-10-14 01:20:42
举报
文章被收录于专栏:小石不识月_Leo小石不识月_Leo

this 的误解

  • this 默认指向函数自己。

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

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

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

this 是什么

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

  • 词法作用域:词法作用域指的是在词法阶段产生的作用域,由书写者在写代码时所写的变量及作用域的位置所决定。引擎根据这些位置信息来查找标识符即变量的位置。例如:无论函数在哪里、如何被调用,它的词法作用域都只由被声明时所处的位置决定。
  • 动态作用域:动态作用域是一个在运行时被动态确定的形式,而不是在静态时被确定。动态作用域不关心函数与作用域如何嵌套或何处声明,只关心它们在何处调用,也就是说。它的作用域链是基于调用栈而非作用域嵌套。例:
代码语言:javascript
复制
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) 。示例:

代码语言:javascript
复制
//全局作用域下
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:

代码语言:javascript
复制
//在对象的属性上声明一个函数
var obj = { 
  foo: function func() {}
};

示例 2:

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

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

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

隐式丢失,即间接引用

示例 1:

代码语言:javascript
复制
var b = {  
  func: function() {}
};
var a = b.func;
a();

示例 2:

代码语言:javascript
复制
var b = {
  func: function() {}
};
function foo(fn) {
  fn();
}
foo(b.func);

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

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

示例 1:

代码语言:javascript
复制
function bar() {}
var b = {
  func: bar
};
var a = b.func; //相当于  var a=bar;
a();

示例 2:

代码语言:javascript
复制
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:

代码语言:javascript
复制
var a = 1;
function func() {
  console.log(this.a);
}
var obj = {
  a: 0
};
func.apply(obj); //0

绑定示例 2:

代码语言:javascript
复制
var a = 1;
function func() {
  console.log(this.a);
}
var obj = {
  a: 0
};
func.call(obj); //0

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

示例代码:

代码语言:javascript
复制
var a = 1;
function func() {
  console.log(this.a);
}
var obj = {
  a: 0
};
setTimeout(func.call(obj), 1000); //立即执行了,无法满足延迟执行的需求

显式绑定中的硬绑定

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

示例:

代码语言:javascript
复制
var a = 1;
function func() {
  console.log(this.a);
}
var obj = {
  a: 0
};
var newFunc = func.bind(obj);
setTimeout(newFunc, 1000); //延迟1秒后打印0

显式绑定中的软绑定

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

示例:

代码语言:javascript
复制
function func() {
  console.log(this.a);
}
var obj = {
  a: 0
};
var o = {
  a: 2
};
var newFunc = func.bind(obj);
newFunc.apply(o); //0

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

示例:

代码语言:javascript
复制
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

核心代码:

代码语言:javascript
复制
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 对象。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-08-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小石不识月 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • this 的误解
  • this 是什么
  • 调用栈
  • this 的绑定规则
    • 默认绑定
      • 隐式绑定
        • 函数方法并不属于对象
          • 隐式丢失,即间接引用
            • 显式绑定
              • 显式绑定中的硬绑定
                • 显式绑定中的软绑定
                  • new 绑定
                    • 优先级
                      • 箭头函数 this 绑定
                      相关产品与服务
                      云开发 CLI 工具
                      云开发 CLI 工具(Cloudbase CLI Devtools,CCLID)是云开发官方指定的 CLI 工具,可以帮助开发者快速构建 Serverless 应用。CLI 工具提供能力包括文件储存的管理、云函数的部署、模板项目的创建、HTTP Service、静态网站托管等,您可以专注于编码,无需在平台中切换各类配置。
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档