JavaScript this指向绑定方式及不适用情况详解

目录
  • 前言
  • 问题复现
  • 调用位置
    • 默认绑定
    • 隐式绑定
    • 显式绑定
    • new 绑定
  • 不适用的情况
  • 总结

前言

JavaScript 中的 this 指向问题对于 web 前端入行不深的人来说是个比较复杂的问题。特写此文章来记录最近遇到的关于匿名函数中 this 指向问题的思考和感悟。

问题复现

最近在研究函数防抖场景时看到如下代码:

function debounce(fn, delay) {
  var timer; // 维护一个 timer
  return function () {
    var _this = this; // 取 debounce 执行作用域的 this
    var args = arguments;
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(function () {
      fn.apply(_this, args); // 用 apply 指向调用 debounce 的对象,相当于 _this.fn(args);
    }, delay);
  };
}

其中有一段代码是 var _this = this 这段代码出现在由 return 返回的匿名函数中,这个时候我就有些懵逼了,因为根据我匮乏的 js 知识,这里的 this 应该是指向全局作用域才对,为什么能像注释那样指向 debounce 执行时的作用域呢?感觉如下所写是否更加合理呢?(事实证明这么写肯定是不对的)

function debounce(fn, delay) {
    var timer
    var _this = this
    return function() {
    ...
    }
}

于是我打算用代码来实测这里的 debounce 执行作用域中的 this 到底指的是什么,它会变化吗?还是根据我的理解只要是像这样类似的匿名函数,其中的 this 都是指向全局的呢? 于是我写下如下代码(关键部分):

body 部分新增一个 button 标签

<button>我是button</button>

script 标签内部代码如下:

//函数防抖
function debounce(fn, delay) {
  var timer; // 维护一个 timer
  return function () {
    var _this = this; // 取 debounce 执行作用域的 this
    var args = arguments;
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(function () {
      fn.apply(_this, args); // 用 apply 指向调用 debounce 的对象,相当于 _this.fn(args);
    }, delay);
  };
}
var btn = document.getElementsByTagName('button')[0]
btn.onclick= debounce(function() {
  console.log(this)
}, 1000)

点击按钮,看看控制台输出 this 到底是谁,按照我之前的理解输出的 this 应该是window 全局对象才对

<button>我是button</button>

出乎意料,这里的 this 输出的是 button 元素,于是我再在上述脚本中新增一个事件绑定:

window.onclick = debounce(function() {
 &nbsp;console.log(this)
}, 1000)

点击页面空白处输入如下:

这次输出的就是 window 了! 看来这里的 this 实际是跟 debounce 函数所返回函数的实际调用者有关,第一次控制台输出的是 button 元素,因为是通过 button 元素来调用该返回函数,第二次调用者就是 widnow,举这段

btn.onclick = dobounce(function() {console.log(this)}, 1000)

代码的例子:

  • 页面初始化完毕后,执行脚本代码,debounce 函数接收一个具体函数(将其命名为 fn 好了)和一个时间间隔参数( intervcal )
  • 进到 debounce 代码内部,return 一个匿名函数,并赋给 btn.onclick,实际上就是事件绑定
  • 所以说当我点击 button 的时候,btn.onclick 的执行代码是这样的:
btn.onclick = function() {
    var _this = this; // 取 debounce 执行作用域的 this
    var args = arguments;
    if (timer) {
        clearTimeout(timer);
    }
    //因为闭包的存在 timer 还是取的 debounce 中的 timer
    timer = setTimeout(function () {
        fn.apply(_this, args); // 用 apply 指向调用 debounce 的对象,相当于 _this.fn(args);
    }, delay);
}

那么这里的 this 指向的就是 button 元素了,为什么呢,以上的例子引出我们今天的主题 - 函数的 this 指向

调用位置

关于函数的 this ,常常有句话,叫做谁调用就指向谁。简单来说 this 的指向跟函数的调用位置紧密相关,要想知道函数调用时 this 到底引用了什么,就应该明确函数的调用位置。一般来说需要通过函数的调用栈来判断来分析出函数真正的调用位置,具体怎么分析呢?除了目测代码外,还也可以借用浏览器的开发者工具( debug 工具),去推断目标函数到底是在哪里调用的,这样才能更准确的知晓this的指向。比如下面这段代码:

function foo() {
  console.log('foo')
}
function bar() {
  console.log('bar')
  foo()
}
bar()

要想知道 foo 函数是由谁调用的,就可以在浏览器中打开调试工具,在 foo 函数中的第一行打一个断点,找到函数的调用栈,然后再找到栈中的第二个元素,这就是真正的调用位置。如下图所示:

从浏览器的调试工具可以找到 foo 函数的真正调用位置。

默认绑定

var a = 2
function foo() {
  var a = 3
  console.log(this.a)
}
foo() // 2

输出结果为 2。因为 foo 函数调用时处于全局环境下(这里是 window ),查看一下浏览器中的调用栈:

调用栈中只有 foo 函数一个元素,说明调用者就是当前的全局环境 window ,所以这里的 this 指向的就是 window,因为最外部的 a 一开始是最为 window.a 声明并赋值的,所以可以理解为this = window; this.a = 2。比较特殊的一点就是,如果在 foo 函数内部采用了严格模式,那么 this 就会绑定到 undefined:

var a = 2
function foo() {
  'use strict'
  var a = 3
  console.log(this.a)
}
foo() //`//Cannot read property 'a' of undefined`

隐式绑定

举如下代码为例:

var a = 2
function foo() {
  console.log(this.a)
}
var obj1 = {
  a:3,
  foo: foo
}
obj.foo() //3

输出结果为 3,说明这里的 this 指向的是 obj1,为什么不再是指向全局环境了呢。在这里就要考虑到调用位置是否存在上下文对象,或者说是否被某个对象拥有或包含。在上述的代码中,foo 函数的引用被赋给了 obj1 的 foo 属性obj1.foo = foo, 并且在 foo 函数被调用时,它的前面也加上了对 obj1 的引用。此时,当函数引用有上下文对象时,隐式绑定规则就会将函数中的this绑定到这个上下文对象,这里的上下文对象就是 obj1。 其实在理解上下文对象时,个人觉得不用那么抽象,它无非就是一个不确定的代名词,简单来说你觉得它是什么,那它就是什么。

显式绑定

默认绑定和隐式绑定在我看来是 js 的一个内置且被动的绑定方式,就是已经这么帮你设定好了,只要符合这两个规则且没有其他规则存在那么 this 的指向就按照这两个规则来。显然,这类被动的绑定方式并不符合实际的代码编写需要,比如我要指定一个函数的 this ,该怎么办呢?这时候就需要显式绑定了。call、apply 会在显式绑定时发挥作用。参考如下代码:

function foo() {
  console.log(this.a)
}
var obj1 = {
  a: 2
}
var a = 3
foo.call(obj1) // 2

输出结果为 2。原因是因为 call 方法改变了 foo 函数运行的 this 指向,将原本 this 指向的 window 全局转为了指向 obj1 ,所以输出的是 2,从这里也可以看出,显示绑定的优先级大于默认绑定。

new 绑定

首先应该明确一点,JavaScript 中的 new 与其他面向类的语言不同,在 js 中 new 后面的只不过是一个普通的函数,仅仅是被 new 操作符调用了而已。使用 new 调用函数时,会执行如下步骤:

  • 创建(或者说构造)一个全新的对象。
  • 这个新对象会被执行 [[Prototype]] 连接。
  • 这个新对象会绑定到函数调用的 this。
  • 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。 代码如下所示:
function foo(a) {
  this.a = a
}
var bar = new foo(2)
console.log(bar.a) // 2

输出结果为 2。

不适用的情况

ES6 中出现了一种特殊的函数:箭头函数。以上的四种规则在箭头函数中都不适用,箭头函数的是根据外层函数或者全局链决定 this 的。其实这也是对以往 ES6 之前的较为复杂的 this 绑定规则的优化和统一,在实际编码的过程中更容易让人理解,当然箭头函数也有缺点,这里就不再展开。

总结

在写这篇总结文章之前,一直对 js 中的 this 问题理解不深,翻了几遍《你不知道的JavaScript》才算真正有所学习和领悟。本文写的并不具体,就 this 绑定时的绑定丢失问题并没有展开叙述,绑定的规则优先级也没有写全,暂时先留个坑,后面再来填坑 !

参考文献

以上就是JavaScript this指向绑定方式及不适用情况详解的详细内容,更多关于JavaScript this指向的资料请关注我们其它相关文章!

(0)

相关推荐

  • 关于vue.js中this.$emit的理解使用

    目录 一.每个 Vue 实例都实现了事件接口 二.注意事项 三.例子及说明 四.总说明 一.每个 Vue 实例都实现了事件接口 即: 1.使用 $on(eventName) 监听事件 2.使用 $emit(eventName, optionalPayload) 触发事件 二.注意事项 1.父组件可以在使用子组件的地方直接用 v-on 来监听子组件触发的事件 2.不能用 $on 监听子组件释放的事件,而必须在模板里直接用 v-on 绑定 三.例子及说明 1.父组件代码及说明 <template>

  • 详解JavaScript原型对象的this指向问题

    目录 一.this指向 二.修改this指向 1.call()方法 2.apply()方法 总结 一.this指向 构造函数中的this 指向实例对象.那么原型对象this的指向呢? 如下: function Student(age,name){ this.age = age; this.name = name; } var that; Student.prototype.score = function(){ console.log('孩子们成绩都很好!'); that = this; } v

  • JS 箭头函数的this指向详解

    箭头函数是ES6中的新增特性,他没有自己的this,其this指向从外层代码库继承. 使用箭头函数时要注意一下几点: 箭头函数不能用作构造函数,用的话会抛出一个错误 无法使用arguments参数,如果要用的话就用rest 无法使用yield命令,所以箭头函数无法用作Generator函数 因为没有自己的this,所以没法通过bind.call.apply来改变this指向 但是这不代表箭头函数的this指向是静态的,我们可以通过改变它外层代码库的this指向来控制 箭头函数的this从外层代码

  • JS 中在严格模式下 this 的指向问题

    目录 前言 一.全局作用域中的this 二.全局作用域中函数中的this 三.对象的函数(方法)中的this 四.构造函数的this 五.事件处理函数中的this 六.内联事件处理函数中的this 七.定时器中的this,指向的是window 参考资料 前言 非严格模式下的 this 指向可能我们会经常遇到,但是严格模式下的 this 指向不是经常碰到,关于严格模式下的 this 指向是怎么样的,都是指向哪些,这篇博文将会很仔细地说清楚. 一.全局作用域中的this 在严格模式下,在全局作用域中

  • Vuejs使用addEventListener的事件如何触发执行函数的this

    目录 使用addEventListener事件触发执行函数的this 如下面的例子 addEventListener中事件函数的this指向问题 看代码 代码2 使用addEventListener事件触发执行函数的this 在普通的dom操作中,若是使用addEventListener 如下面的例子 <!-- index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta

  • Vue.js中this如何取到data和method里的属性详解

    目录 准备工作 调试源码 initMethods initData 结束语 本篇文章介绍的是Vue.js如何取到data和methods里的属性? 准备工作 克隆源码到本地 git clone https://github.com/vuejs/vue.git 下载完毕后,用vscode打开,目光移动到package.json的scripts属性,我们看到有dev和build,dev会启动一个开发环境的服务,也就是说,我们在源码里做的改动,都会及时生效.build就是打包.和我们平时开发Vue.j

  • 普通js文件里面如何访问vue实例this指针

    目录 普通js文件里访问vue实例this指针 then使用函数无法访问vue实例化的this 问题 原因 解决 普通js文件里访问vue实例this指针 main.js 文件,暴露出vue实例 Vue.use(VueAxios) const vue = new Vue({   router,   store,   created: bootstrap,   render: h => h(App) }).$mount('#app') export default vue js 文件中,使用实例

  • JavaScript this指向绑定方式及不适用情况详解

    目录 前言 问题复现 调用位置 默认绑定 隐式绑定 显式绑定 new 绑定 不适用的情况 总结 前言 JavaScript 中的 this 指向问题对于 web 前端入行不深的人来说是个比较复杂的问题.特写此文章来记录最近遇到的关于匿名函数中 this 指向问题的思考和感悟. 问题复现 最近在研究函数防抖场景时看到如下代码: function debounce(fn, delay) { var timer; // 维护一个 timer return function () { var _this

  • JavaScript变量声明的var、let、const详解

    目录 前言 内容 JavaScript的变量声明 var的变量声明 变量声明在函数作用域中 变量重复声明 变量声明提升 怪异危险的var let和const的变量声明 块级作用域 不可重复声明 暂时性死区 使用好let和const 总结 参考资料 前言 一个程序语言在运行的过程中,变量的声明在整个程序的生命周期中,是不断在进行的过程.任何程序的计算都会涉及至少一个变量,而计算的结果的则可能会涉及到另外的一个或者多个变量.变量在使用前是要声明,变量声明的过程在计算机的底层,牵涉到的是内存空间和内存

  • JavaScript箭头函数与普通函数的区别示例详解

    目录 箭头函数与普通函数的区别 箭头函数的理解 箭头函数里的this指向 总结 箭头函数与普通函数的区别 要讨论箭头函数和普通函数的区别,首先来看看两者的基本格式 普通函数和箭头共同点就是圆括号和大括号,圆括号里面一般放置参数,大括号一般放置函数主体,很明显箭头函数不需要写那么长,举个例子,有一个数组,使用map方法为数组的每个元素增加字符 let arr=['昨天','今天','明天'] let newarr=arr.map(function(item){ return item+='放假'

  • JavaScript中的ajax功能的概念和示例详解

    AJAX即"Asynchronous Javascript And XML"(异步JavaScript和XML). 个人理解:ajax就是无刷新提交,然后得到返回内容. 对应的不使用ajax时的传统网页如果需要更新内容(或用php做处理时),必须重载整个网页页面. 示例: html代码如下 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>

  • javascript 中事件冒泡和事件捕获机制的详解

    javascript 中事件冒泡和事件捕获机制的详解 二者作用:描述事件触发时序问题 事件捕获:从document到触发事件的那个节点,即自上而下的去触发事件---由外到内 事件冒泡:自下而上的去触发事件---由内到外 绑定事件方法的第三个参数,就是控制事件触发顺序是否为事件捕获 true,事件捕获:false,事件冒泡 一般默认false,即事件冒泡 Jquery的e.stopPropagation会阻止冒泡,意思就是到DOM为止,祖先级的事件就不要触发了 下面是我尝试的例子: <!DOCTY

  • vue实现绑定事件的方法实例代码详解

    一.前言 vuejs中的事件绑定,使用<v-on:事件名 = 函数名>来完成的,这里函数名是定义在Vue实例中的methods对象中的,Vue实例可以直接访问其中的方法. 二.事件绑定方式 1. 直接在标签中写js方法  <button v-on:click="alert('hi')">执行方法的第一种写法</button> 2.调用method的办法 <button v-on:click="run()">执行方法的第

  • JavaScript数组及非数组对象的深浅克隆详解原理

    目录 什么是浅克隆.深克隆 1.对数组进行克隆 1.1 浅克隆 1.2 深克隆 2.对非数组对象进行克隆 2.1 浅克隆 2.2 深克隆 3.整合深克隆函数 什么是浅克隆.深克隆 浅克隆:直接将存储在栈中的值赋值给对应变量,如果是基本数据类型,则直接赋值对应的值,如果是引用类型,则赋值的是地址. 深克隆:将数据赋值给对应的变量,从而产生一个与源数据不相干的新数据(数据地址已变化).即对象各个层级的属性. JavaScript中基本数据类型使用符号"="可以进行克隆,引用数据类型使用符号

  • JavaScript变量or循环中的var和let详解

    目录 在for循环中使用var声明初始化带来的问题 解决方法 使用闭包 使用let变量初始化 for循环怎么处理用let和var声明的初始化变量? 总结 在for循环中使用var声明初始化带来的问题 // 一道经典面试题: var funcs = []; for (var i = 0; i < 3; i++) { funcs[i] = function() { console.log("My value: " + i) }; } for (var j = 0; j < 3;

  • Java 利用dom方式读取、创建xml详解及实例代码

    Java 利用dom方式读取.创建xml详解 1.创建一个接口 XmlInterface.Java public interface XmlInterface { /** * 建立XML文档 * @param fileName 文件全路径名称 */ public void createXml(String fileName); /** * 解析XML文档 * @param fileName 文件全路径名称 */ public void parserXml(String fileName); }

  • JavaScript中自带的 reduce()方法使用示例详解

    1.方法说明 , Array的reduce()把一个函数作用在这个Array的[x1, x2, x3...]上,这个函数必须接收两个参数,reduce()把结果继续和序列的下一个元素做累积计算,其效果就是: [x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4) 2. 使用示例 'use strict'; function string2int(s){ if(!s){ alert('the params empty'); return; } if

随机推荐