彻底搞懂 JS 中 this 机制

彻底搞懂 JS 中 this 机制

摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.com/jasonGeng88/blog

目录

  • this 是什么
  • this 的四种绑定规则
  • 绑定规则的优先级
  • 绑定例外
  • 扩展:箭头函数

this 是什么

理解this之前, 先纠正一个观点,this 既不指向函数自身,也不指函数的词法作用域。如果仅通过this的英文解释,太容易产生误导了。它实际是在函数被调用时才发生的绑定,也就是说this具体指向什么,取决于你是怎么调用的函数。

this 的四种绑定规则

this的4种绑定规则分别是:默认绑定、隐式绑定、显示绑定、new 绑定。优先级从低到高。

默认绑定

什么叫默认绑定,即没有其他绑定规则存在时的默认规则。这也是函数调用中最常用的规则。

来看这段代码:

function foo() { 
}       console.log( this.a );var a = 2; 
foo(); //打印的是什么?

foo() 打印的结果是2。

因为foo()是直接调用的(独立函数调用),没有应用其他的绑定规则,这里进行了默认绑定,将全局对象绑定this上,所以this.a 就解析成了全局变量中的a,即2。

注意:在严格模式下(strict mode),全局对象将无法使用默认绑定,即执行会报undefined的错误

function foo() { "use strict";console.log( this.a );
}var a = 2; 
foo(); // Uncaught TypeError: Cannot read property 'a' of undefined

隐式绑定

除了直接对函数进行调用外,有些情况是,函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。

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

obj.foo() 打印的结果是3。

这里foo函数被当做引用属性,被添加到obj对象上。这里的调用过程是这样的:

获取obj.foo属性 -> 根据引用关系找到foo函数,执行调用

所以这里对foo的调用存在上下文对象obj,this进行了隐式绑定,即this绑定到了obj上,所以this.a被解析成了obj.a,即3。

多层调用链

function foo() { console.log( this.a );
}var a = 2;var obj1 = { a: 4,foo: foo 
};var obj2 = { a: 3,obj1: obj1
};obj2.obj1.foo(); //?

obj2.obj1.foo() 打印的结果是4。

同样,我们看下函数的调用过程:

先获取obj2.obj1 -> 通过引用获取到obj1对象,再访问 obj1.foo -> 最后执行foo函数调用

这里调用链不只一层,存在obj1、obj2两个对象,那么隐式绑定具体会绑哪个对象。这里原则是获取最后一层调用的上下文对象,即obj1,所以结果显然是4(obj1.a)。

隐式丢失(函数别名)

注意:这里存在一个陷阱,大家在分析调用过程时,要特别小心

先看个代码:

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

<font color="red">bar() 打印的结果是2。</font>

为什么会这样,obj.foo 赋值给bar,那调用bar()为什么没有触发隐式绑定,使用的是默认绑定呢。

这里有个概念要理解清楚,obj.foo 是引用属性,赋值给bar的实际上就是foo函数(即:bar指向foo本身)。

那么,实际的调用关系是:通过bar找到foo函数,进行调用。整个调用过程并没有obj的参数,所以是默认绑定,全局属性a。

隐式丢失(回调函数)

function foo() { console.log( this.a );
}var a = 2;var obj = { a: 3,foo: foo 
};setTimeout( obj.foo, 100 ); // ?

<font color="red">打印的结果是2。</font>

同样的道理,虽然参传是obj.foo,因为是引用关系,所以传参实际上传的就是foo对象本身的引用。对于setTimeout的调用,还是 setTimeout -> 获取参数中foo的引用参数 -> 执行 foo 函数,中间没有obj的参与。这里依旧进行的是默认绑定。


显示绑定

相对隐式绑定,this值在调用过程中会动态变化,可是我们就想绑定指定的对象,这时就用到了显示绑定。

显示绑定主要是通过改变对象的prototype关联对象,这里不展开讲。具体使用上,可以通过这两个方法call(...)或apply(...)来实现(大多数函数及自己创建的函数默认都提供这两个方法)。

call与apply是同样的作用,区别只是其他参数的设置上

function foo() { console.log( this.a );
}var a = 2;var obj1 = { a: 3,
};var obj2 = { a: 4,
};
foo.call( obj1 ); // ?
foo.call( obj2 ); // ?

打印的结果是3, 4。

这里因为显示的申明了要绑定的对象,所以this就被绑定到了obj上,打印的结果自然就是obj1.a 和obj2.a。

硬绑定

function foo() { console.log( this.a );
}var a = 2;var obj1 = { a: 3,
};var obj2 = { a: 4,
};var bar = function(){foo.call( obj1 );
}bar(); // 3
setTimeout( bar, 100 ); // 3bar.call( obj2 ); // 这是多少

前面两个(函数别名、回调函数)打印3,因为显示绑定了,没什么问题。

最后一个打印是3。

这里需要注意下,虽然bar被显示绑定到obj2上,对于bar,function(){...} 中的this确实被绑定到了obj2,而foo因为通过foo.call( obj1 )已经显示绑定了obj1,所以在foo函数内,this指向的是obj1,不会因为bar函数内指向obj2而改变自身。所以打印的是obj1.a(即3)。


new 绑定

js中的new操作符,和其他语言中(如JAVA)的new机制是不一样的。js中,它就是一个普通函数调用,只是被new修饰了而已。

使用new来调用函数,会自动执行如下操作:

  1. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

从第三点可以看出,this指向的就是对象本身。

看个代码:

function foo(a) { this.a = a;
}var a = 2;var bar1 = new foo(3);
console.log(bar1.a); // ?var bar2 = new foo(4);
console.log(bar2.a); // ?

最后一个打印是3, 4。

因为每次调用生成的是全新的对象,该对象又会自动绑定到this上,所以答案显而易见。

绑定规则优先级

上面也说过,这里在重复一下。优先级是这样的,以按照下面的顺序来进行判断:

 数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是 指定的对象。数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到 全局对象。var bar = foo()

规则例外

在显示绑定中,对于null和undefined的绑定将不会生效。

代码如下:

function foo() { console.log( this.a );
}
foo.call( null ); // 2
foo.call( undefined ); // 2

这种情况主要是用在不关心this的具体绑定对象(用来忽略this),而传入null实际上会进行默认绑定,导致函数中可能会使用到全局变量,与预期不符。

所以对于要忽略this的情况,可以传入一个空对象ø,该对象通过Object.create(null)创建。这里不用{}的原因是,ø是真正意义上的空对象,它不创建Object.prototype委托,{}和普通对象一样,有原型链委托关系。

1. 这里传null的一种具体使用场景是函数柯里化的使用

扩展:箭头函数

最后,介绍一下ES6中的箭头函数。通过“=>”而不是function创建的函数,叫做箭头函数。它的this绑定取决于外层(函数或全局)作用域。

case 1 (正常调用)

  • 普通函数
function foo(){     console.log( this.a );
}var a = 2;var obj = { a: 3,foo: foo 
};obj.foo(); //3
  • 箭头函数
var foo = () => {     console.log( this.a );
}var a = 2;var obj = { a: 3,foo: foo 
};obj.foo(); //2
foo.call(obj); //2 ,箭头函数中显示绑定不会生效

case 2 (函数回调)

  • 普通函数
function foo(){ return function(){console.log( this.a );}    
}var a = 2;var obj = { a: 3,foo: foo 
};var bar = obj.foo();
bar(); //2
  • 箭头函数
function foo(){ return () => {console.log( this.a );}    
}var a = 2;var obj = { a: 3,foo: foo 
};var bar = obj.foo();
bar(); //3

通过上面两个列子,我们看到箭头函数的this绑定<font color="red">只取决于外层(函数或全局)的作用域</font>,对于前面的4种绑定规则是不会生效的。它也是作为this机制的一种替换,解决之前this绑定过程各种规则带来的复杂性。

注意:对于ES6之前,箭头函数的替换版本是这样的

// es6
function foo(){ return () => {console.log( this.a );}   
}var a = 2;var obj = { a: 3,foo: foo 
};var bar = obj.foo();
bar(); //3

通过上面两个列子,我们看到箭头函数的this绑定<font color="red">只取决于外层(函数或全局)的作用域</font>,对于前面的4种绑定规则是不会生效的。它也是作为this机制的一种替换,解决之前this绑定过程各种规则带来的复杂性。

注意:对于ES6之前,箭头函数的替换版本是这样的

// es6
function foo(){ return () => {console.log( this.a );}   
}// es6之前的替代方法
function foo(){ var self = this;return () => {console.log( self.a );}   
}

总结

我们在使用js的过程中,对于this的理解往往觉得比较困难,再调试过程中有时也会出现一些不符合预期的现象。很多时候,我们都是通过一些变通的方式(如:使用具体对象替换this)来规避的问题。可问题一直存在那儿,我们没有真正的去理解和解决它。

本文主要参考了《你不知道的JavaScript(上卷)》,对this到底是什么,具体怎么绑定的,有什么例外情况以及ES6中的一个优化方向,来彻底搞清楚我们一直使用的this到底是怎么玩的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/390741.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

⚡如何在2分钟内将GraphQL服务器添加到RESTful Express.js API

You can get a lot done in 2 minutes, like microwaving popcorn, sending a text message, eating a cupcake, and hooking up a GraphQL server.您可以在2分钟内完成很多工作&#xff0c;例如微波炉爆米花&#xff0c;发送短信&#xff0c; 吃蛋糕以及连接GraphQL服务器 。 …

leetcode 1744. 你能在你最喜欢的那天吃到你最喜欢的糖果吗?

给你一个下标从 0 开始的正整数数组 candiesCount &#xff0c;其中 candiesCount[i] 表示你拥有的第 i 类糖果的数目。同时给你一个二维数组 queries &#xff0c;其中 queries[i] [favoriteTypei, favoriteDayi, dailyCapi] 。 你按照如下规则进行一场游戏&#xff1a; 你…

回归分析_回归

回归分析Machine learning algorithms are not your regular algorithms that we may be used to because they are often described by a combination of some complex statistics and mathematics. Since it is very important to understand the background of any algorith…

ruby nil_Ruby中的数据类型-True,False和Nil用示例解释

ruby niltrue, false, and nil are special built-in data types in Ruby. Each of these keywords evaluates to an object that is the sole instance of its respective class.true &#xff0c; false和nil是Ruby中的特殊内置数据类型。 这些关键字中的每一个都求值为一个对…

浅尝flutter中的动画(淡入淡出)

在移动端开发中&#xff0c;经常会有一些动画交互&#xff0c;比如淡入淡出,效果如图&#xff1a; 因为官方封装好了AnimatedOpacity Widget&#xff0c;开箱即用&#xff0c;所以我们用起来很方便&#xff0c;代码量很少&#xff0c;做少量配置即可&#xff0c;所以&#xff0…

数据科学还是计算机科学_何时不使用数据科学

数据科学还是计算机科学意见 (Opinion) 目录 (Table of Contents) Introduction 介绍 Examples 例子 When You Should Use Data Science 什么时候应该使用数据科学 Summary 摘要 介绍 (Introduction) Both Data Science and Machine Learning are useful fields that apply sev…

空间复杂度 用什么符号表示_什么是大O符号解释:时空复杂性

空间复杂度 用什么符号表示Do you really understand Big O? If so, then this will refresh your understanding before an interview. If not, don’t worry — come and join us for some endeavors in computer science.您真的了解Big O吗&#xff1f; 如果是这样&#xf…

leetcode 523. 连续的子数组和

给你一个整数数组 nums 和一个整数 k &#xff0c;编写一个函数来判断该数组是否含有同时满足下述条件的连续子数组&#xff1a; 子数组大小 至少为 2 &#xff0c;且 子数组元素总和为 k 的倍数。 如果存在&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 …

Docker学习笔记 - Docker Compose

一、概念 Docker Compose 用于定义运行使用多个容器的应用&#xff0c;可以一条命令启动应用&#xff08;多个容器&#xff09;。 使用Docker Compose 的步骤&#xff1a; 定义容器 Dockerfile定义应用的各个服务 docker-compose.yml启动应用 docker-compose up二、安装 Note t…

创建shell脚本

1.写一个脚本 a) 用touch命令创建一个文件&#xff1a;touch my_script b) 用vim编辑器打开my_script文件&#xff1a;vi my_script c) 用vim编辑器编辑my_script文件,内容如下&#xff1a; #!/bin/bash 告诉shell使用什么程序解释脚本 #My first script l…

线性回归算法数学原理_线性回归算法-非数学家的高级数学

线性回归算法数学原理内部AI (Inside AI) Linear regression is one of the most popular algorithms used in different fields well before the advent of computers. Today with the powerful computers, we can solve multi-dimensional linear regression which was not p…

您应该在2020年首先学习哪种编程语言? ɐʌɐɾdıɹɔsɐʌɐɾ:ɹǝʍsuɐ

Most people’s journey toward learning to program starts with a single late-night Google search.大多数人学习编程的旅程都是从一个深夜Google搜索开始的。 Usually it’s something like “Learn ______”通常它类似于“学习______” But how do they decide which la…

Linux 概述

UNIX发展历程 第一个版本是1969年由Ken Thompson&#xff08;UNIX之父&#xff09;在AT& T贝尔实验室实现Ken Thompson和Dennis Ritchie&#xff08;C语言之父&#xff09;使用C语言对整个系统进行了再加工和编写UNIX的源代码属于SCO公司&#xff08;AT&T ->Novell …

课程一(Neural Networks and Deep Learning),第四周(Deep Neural Networks)—— 0.学习目标...

Understand the key computations underlying deep learning, use them to build and train deep neural networks, and apply it to computer vision. 学习目标 See deep neural networks as successive blocks put one after each otherBuild and train a deep L-layer Neura…

使用ActionTrail Python SDK

ActionTrail提供官方的Python SDK。本文将简单介绍一下如何使用ActionTrail的Python SDK。 安装Aliyun Core SDK。 pip install aliyun-python-sdk-core 安装ActionTrail Python SDK。 pip install aliyun-python-sdk-actiontrail 下面是测试的代码。调用LookupEventsRequest获…

泰坦尼克:机器从灾难中学习_用于灾难响应的机器学习研究:什么才是好的论文?...

泰坦尼克:机器从灾难中学习For the first time in 2021, a major Machine Learning conference will have a track devoted to disaster response. The 16th Conference of the European Chapter of the Association for Computational Linguistics (EACL 2021) has a track on…

github持续集成的设置_如何使用GitHub Actions和Puppeteer建立持续集成管道

github持续集成的设置Lately Ive added continuous integration to my blog using Puppeteer for end to end testing. My main goal was to allow automatic dependency updates using Dependabot. In this guide Ill show you how to create such a pipeline yourself. 最近&…

shell与常用命令

虚拟控制台 一台计算机的输入输出设备就是一个物理的控制台 &#xff1b; 如果在一台计算机上用软件的方法实现了多个互不干扰独立工作的控制台界面&#xff0c;就是实现了多个虚拟控制台&#xff1b; Linux终端的工作方式是字符命令行方式&#xff0c;用户通过键盘输入命令进…

C中的malloc:C中的动态内存分配

什么是C中的malloc()&#xff1f; (What is malloc() in C?) malloc() is a library function that allows C to allocate memory dynamically from the heap. The heap is an area of memory where something is stored.malloc()是一个库函数&#xff0c;它允许C从堆动态分配…

Linux文本编辑器

Linux文本编辑器 Linux系统下有很多文本编辑器。 按编辑区域&#xff1a; 行编辑器 ed 全屏编辑器 vi 按运行环境&#xff1a; 命令行控制台编辑器 vi X Window图形界面编辑器 gedit ed 它是一个很古老的行编辑器&#xff0c;vi这些编辑器都是ed演化而来。 每次只能对一…