详解JavaScript的this指向和绑定


Posted in Javascript onSeptember 08, 2020

注意: 本文属于基础篇,请大神绕路。如果你不够了解,或者了解的还不完整,那么可以通过本文来复习一下。

this 指向的类型

刚开始学习 JavaScript 的时候,this 总是最能让人迷惑,下面我们一起看一下在 JavaScript 中应该如何确定 this 的指向。

this 是在函数被调用时确定的,它的指向完全取决于函数调用的地方,而不是它被声明的地方(除箭头函数外)。当一个函数被调用时,会创建一个执行上下文,它包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息,this 就是这个记录的一个属性,它会在函数执行的过程中被用到。

this 在函数的指向有以下几种场景:

  1. 作为构造函数被 new 调用
  2. 作为对象的方法使用
  3. 作为函数直接调用
  4. 被 call、apply、bind 调用
  5. 箭头函数中的 this

下面我们分别来讨论一下这些场景中 this 的指向。

1.new 绑定

函数如果作为构造函数使用 new 调用时, this 绑定的是新创建的构造函数的实例。

function Foo() {
 console.log(this)
}

var bar = new Foo() // 输出: Foo 实例,this 就是 bar

实际上使用 new 调用构造函数时,会依次执行下面的操作:

  • 创建一个新对象
  • 构造函数的 prototype 被赋值给这个新对象的 __proto__
  • 将新对象赋给当前的 this
  • 执行构造函数
  • 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象,如果返回的不是对象将被忽略

2.显式绑定

通过 call、apply、bind 我们可以修改函数绑定的 this,使其成为我们指定的对象。通过这些方法的第一个参数我们可以显式地绑定 this。

function foo(name, price) {
 this.name = name
 this.price = price
}

function Food(category, name, price) {
 foo.call(this, name, price) // call 方式调用
 // foo.apply(this, [name, price])  // apply 方式调用
 this.category = category
}

new Food('食品', '汉堡', '5块钱')

// 浏览器中输出: {name: "汉堡", price: "5块钱", category: "食品"}

call 和 apply 的区别是 call 方法接受的是参数列表,而 apply 方法接受的是一个参数数组。

func.call(thisArg, arg1, arg2, ...)    // call 用法
func.apply(thisArg, [arg1, arg2, ...])   // apply 用法

而 bind 方法是设置 this 为给定的值,并返回一个新的函数,且在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。

func.bind(thisArg[, arg1[, arg2[, ...]]])  // bind 用法

举个例子:

var food = {
 name: '汉堡',
 price: '5块钱',
 getPrice: function (place) {
  console.log(place + this.price)
 },
}

food.getPrice('KFC ') // 浏览器中输出: "KFC 5块钱"

var getPrice1 = food.getPrice.bind({ name: '鸡腿', price: '7块钱' }, '肯打鸡 ')
getPrice1() // 浏览器中输出: "肯打鸡 7块钱"

关于 bind 的原理,我们可以使用 apply 方法自己实现一个 bind 看一下:

// ES5 方式
Function.prototype.bind =
 Function.prototype.bind ||
 function () {
  var self = this
  var rest1 = Array.prototype.slice.call(arguments)
  var context = rest1.shift()
  return function () {
   var rest2 = Array.prototype.slice.call(arguments)
   return self.apply(context, rest1.concat(rest2))
  }
 }

// ES6 方式
Function.prototype.bind =
 Function.prototype.bind ||
 function (...rest1) {
  const self = this
  const context = rest1.shift()
  return function (...rest2) {
   return self.apply(context, [...rest1, ...rest2])
  }
 }

ES6 方式用了一些 ES6 的知识比如 rest 参数、数组解构。

注意: 如果你把 null 或 undefined 作为 this 的绑定对象传入 call、apply、bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。

var a = 'hello'

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

foo.call(null) // 浏览器中输出: "hello"

3.隐式绑定

函数是否在某个上下文对象中调用,如果是的话 this 绑定的是那个上下文对象。

var a = 'hello'

var obj = {
 a: 'world',
 foo: function () {
  console.log(this.a)
 },
}

obj.foo() // 浏览器中输出: "world"

上面代码中,foo 方法是作为对象的属性调用的,那么此时 foo 方法执行时,this 指向 obj 对象。也就是说,此时 this 指向调用这个方法的对象,如果嵌套了多个对象,那么指向最后一个调用这个方法的对象:

var a = 'hello'

var obj = {
 a: 'world',
 b: {
  a: 'China',
  foo: function () {
   console.log(this.a)
  },
 },
}

obj.b.foo() // 浏览器中输出: "China"

最后一个对象是 obj 上的 b,那么此时 foo 方法执行时,其中的 this 指向的就是 b 对象。

4.默认绑定

函数独立调用,直接使用不带任何修饰的函数引用进行调用,也是上面几种绑定途径之外的方式。非严格模式下 this 绑定到全局对象(浏览器下是 winodw,node 环境是 global),严格模式下 this 绑定到 undefined (因为严格模式不允许 this 指向全局对象)。

var a = 'hello'

function foo() {
 var a = 'world'
 console.log(this.a)
 console.log(this)
}

foo() // 相当于执行 window.foo()

// 浏览器中输出: "hello"
// 浏览器中输出: Window 对象

上面代码中,变量 a 被声明在全局作用域,成为全局对象 window 的一个同名属性。函数 foo 被执行时,this 此时指向的是全局对象,因此打印出来的 a 是全局对象的属性。

注意有一种情况:

var a = 'hello'

var obj = {
 a: 'world',
 foo: function () {
  console.log(this.a)
 },
}

var bar = obj.foo

bar() // 浏览器中输出: "hello"

此时 bar 函数,也就是 obj 上的 foo 方法为什么又指向了全局对象呢,是因为 bar 方法此时是作为函数独立调用的,所以此时的场景属于默认绑定,而不是隐式绑定。这种情况和把方法作为回调函数的场景类似:

var a = 'hello'

var obj = {
 a: 'world',
 foo: function () {
  console.log(this.a)
 },
}

function func(fn) {
 fn()
}

func(obj.foo) // 浏览器中输出: "hello"

参数传递实际上也是一种隐式的赋值,只不过这里 obj.foo 方法是被隐式赋值给了函数 func 的形参 fn,而之前的情景是自己赋值,两种情景实际上类似。这种场景我们遇到的比较多的是 setTimeout 和 setInterval,如果回调函数不是箭头函数,那么其中的 this 指向的就是全局对象.

其实我们可以把默认绑定当作是隐式绑定的特殊情况,比如上面的 bar(),我们可以当作是使用 window.bar() 的方式调用的,此时 bar 中的 this 根据隐式绑定的情景指向的就是 window。

this 绑定的优先级

this 存在多个使用场景,那么多个场景同时出现的时候,this 到底应该如何指向呢。这里存在一个优先级的概念,this 根据优先级来确定指向。优先级:new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定

所以 this 的判断顺序:

  • new 绑定: 函数是否在 new 中调用?如果是的话 this 绑定的是新创建的对象;
  • 显式绑定: 函数是否是通过 bind、call、apply 调用?如果是的话,this 绑定的是指定的对象;
  • 隐式绑定: 函数是否在某个上下文对象中调用?如果是的话,this 绑定的是那个上下文对象;
  • 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到全局对象。

箭头函数中的 this

箭头函数 是根据其声明的地方来决定 this 的,它是 ES6 的知识点。

箭头函数的 this 绑定是无法通过 call、apply、bind 被修改的,且因为箭头函数没有构造函数 constructor,所以也不可以使用 new 调用,即不能作为构造函数,否则会报错。

var a = 'hello'

var obj = {
 a: 'world',
 foo: () => {
  console.log(this.a)
 },
}

obj.foo() // 浏览器中输出: "hello"

我们可以看看 ECMAScript 标准中对箭头函数的描述。

原文:

An Arrow Function does not define local bindings for arguments, super, this, or new.target. Any reference to arguments, super, this, or new.target within an ArrowFunction must resolve to a binding in a lexically enclosing environment. Typically this will be the Function Environment of an immediately enclosing function.

翻译:

箭头函数不为 arguments、super、this 或 new.target 定义本地绑定。箭头函数中对 arguments、super、this 或 new.target 的任何引用都解析为当前所在词法作用域中的绑定。通常,这是箭头函数所在的函数作用域。

— ECMAScript Language Specification - Arrow Function | ECMA 标准 - 箭头函数

一个 this 的小练习

用一个小练习来实战一下:

var a = 20

var obj = {
 a: 40,
 foo: () => {
  console.log(this.a)

  function func() {
   this.a = 60
   console.log(this.a)
  }

  func.prototype.a = 50
  return func
 },
}

var bar = obj.foo() // 浏览器中输出: 20
bar() // 浏览器中输出: 60
new bar() // 浏览器中输出: 60

稍微解释一下:

1)var a = 20 这句在全局变量 window 上创建了个属性 a 并赋值为 20;

2)首先执行的是 obj.foo(),这是一个箭头函数,箭头函数不创建新的函数作用域直接沿用语句外部的作用域,因此 obj.foo() 执行时箭头函数中 this 是全局 window,首先打印出 window 上的属性 a 的值 20,箭头函数返回了一个原型上有个值为 50 的属性 a 的函数对象 func 给 bar;

3)继续执行的是 bar(),这里执行的是刚刚箭头函数返回的闭包 func,其内部的 this 指向 window,因此 this.a 修改了 window.a 的值为 60 并打印出来;

4)然后执行的是 new bar(),根据之前的表述,new 操作符会在 func 函数中创建一个继承了 func 原型的实例对象并用 this 指向它,随后 this.a = 60 又在实例对象上创建了一个属性 a,在之后的打印中已经在实例上找到了属性 a,因此就不继续往对象原型上查找了,所以打印出第三个 60。

如果把上面例子的箭头函数换成普通函数呢,结果会是什么样?

var a = 20

var obj = {
 a: 40,
 foo: function () {
  console.log(this.a)

  function func() {
   this.a = 60
   console.log(this.a)
  }

  func.prototype.a = 50
  return func
 },
}

var bar = obj.foo() // 浏览器中输出: 40
bar() // 浏览器中输出: 60
new bar() // 浏览器中输出: 60

这个例子就不详细讲解了。

如果把上面两个例子弄懂原理,基本上 this 的指向就掌握的差不多啦~

以上就是详解JavaScript的this指向和绑定的详细内容,更多关于JavaScript的this指向和绑定的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
jQuery textarea的长度进行验证
May 06 Javascript
JS 无法通过W3C验证的处理方法
Mar 09 Javascript
JavaScript中的Primitive对象封装介绍
Dec 31 Javascript
JS访问SWF的函数用法实例
Jul 01 Javascript
jQuery使用animate创建动画用法实例
Aug 07 Javascript
javascript入门教程基础篇
Nov 16 Javascript
jquery实现两边飘浮可关闭的对联广告
Nov 27 Javascript
Angularjs全局变量被作用域监听的正确姿势
Feb 06 Javascript
js判断radiobuttonlist的选中值显示/隐藏其它模块的实现方法
Aug 25 Javascript
vue页面切换到滚动页面显示顶部的实例
Mar 13 Javascript
javascript中函数的写法实例代码详解
Oct 28 Javascript
将Vue组件库更换为按需加载的方法步骤
May 06 Javascript
vue点击按钮实现简单页面的切换
Sep 08 #Javascript
Vue filter 过滤器、以及在table中的使用介绍
Sep 07 #Javascript
VUE中setTimeout和setInterval自动销毁案例
Sep 07 #Javascript
vue a标签点击实现赋值方式
Sep 07 #Javascript
JavaScript实现多球运动效果
Sep 07 #Javascript
JavaScript实现世界各地时间显示
Sep 07 #Javascript
VUE UPLOAD 通过ACTION返回上传结果操作
Sep 07 #Javascript
You might like
帖几个PHP的无限分类实现想法~
2007/01/02 PHP
PHP函数in_array()使用详解
2014/08/20 PHP
分享ThinkPHP3.2中关联查询解决思路
2015/09/20 PHP
PHP+Ajax 检测网络是否正常实例详解
2016/12/16 PHP
php进行md5加密简单实例方法
2019/09/19 PHP
js 金额格式化来回转换示例
2014/02/23 Javascript
jQuery实现个性翻牌效果导航菜单的方法
2015/03/09 Javascript
vue2.0+webpack环境的构造过程
2016/11/08 Javascript
jQuery实现表格元素动态创建功能
2017/01/09 Javascript
JavaScript中的子窗口与父窗口的互相调用问题
2017/02/08 Javascript
nodejs开发微信小程序实现密码加密
2017/07/11 NodeJs
react router 4.0以上的路由应用详解
2017/09/21 Javascript
element-ui组件table实现自定义筛选功能的示例代码
2019/03/15 Javascript
node.js实现上传文件功能
2019/07/15 Javascript
基于js判断浏览器是否支持webGL
2020/04/18 Javascript
JavaScript进阶(一)变量声明提升实例分析
2020/05/09 Javascript
VSCode launch.json配置详细教程
2020/06/18 Javascript
微信小程序实现上传多张图片、删除图片
2020/07/29 Javascript
js实现3D旋转效果
2020/08/18 Javascript
[06:13]DOTA2进化论(修改版)
2013/10/08 DOTA
python在linux中输出带颜色的文字的方法
2014/06/19 Python
Python3.4解释器用法简单示例
2019/03/22 Python
Python二维数组实现求出3*3矩阵对角线元素的和示例
2019/11/29 Python
python实现密码强度校验
2020/03/18 Python
python 写一个性能测试工具(一)
2020/10/24 Python
J2EE的优越性主要表现在哪些方面
2016/03/28 面试题
劳资员岗位职责
2013/11/11 职场文书
《小池塘》教学反思
2014/02/28 职场文书
党员教师四风自我剖析材料
2014/09/30 职场文书
介绍信的写法
2015/01/31 职场文书
保险公司反洗钱宣传活动总结
2015/05/08 职场文书
工程项目合作意向书
2015/05/08 职场文书
导游词之吉林花园山
2019/10/17 职场文书
python开发制作好看的时钟效果
2022/05/02 Python
php解析非标准json、非规范json的方式实例
2022/05/10 PHP
postgresql之greenplum字符串去重拼接方式
2023/05/08 PostgreSQL