博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
深入浅出JavaScript的this机制
阅读量:7100 次
发布时间:2019-06-28

本文共 8353 字,大约阅读时间需要 27 分钟。

本文发于,转载请注明出处,谢谢。

「this 是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。」

说到this,可能要涉及到一点JavaScript基础:

「词法作用域」与「动态作用域」

通常来说,作用域一共有两种主要的工作模型。

  • 词法作用域
  • 动态作用域

词法作用域:定义在词法阶段的作用域,也就是说词法作用域是由你在写代码时将变量和块作用域写在哪里决定的

动态作用域:动态作用域并不关心函数和作用域是如何声明以及在任何处声明的,只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套

JavaScript采用的是词法作用域,大多数时候,对作用域产生混乱的主要原因是分不清楚应该按照函数位置的嵌套顺序,还是按照函数的调用顺序进行变量查找。再加上this机制的干扰,使得变量查找极易出错。


下面我们就深入了解一下JavaScript的this机制

this 的四种绑定规则

在JavaScript中,this绑定规则有四种:

  • 默认绑定

  • 隐式绑定

  • 显式绑定

  • new 绑定

默认绑定

默认绑定是JavaScript中this绑定最基本、最直接的一种绑定方式,简而言之,就是直接调用函数

function foo(){    console.log(this.a); // 输出: 2 直接调用 this指向全局对象window}var a = 2;foo(); // 等价于 window.foo(); foo()由 window直接调用,故this指向 window 复制代码

注:在浏览器中全局对象是BOM的window对象,Node中是global对象

Node环境下,没有window对象,只有全局的global对象复制代码

严格模式下this指向会有一些偏差,请注意区分

1、严格模式下,独立调用 的函数的this为undefined

2、非严格模式下,使用call、apply时,null、undefined会被转换为全局对象(window/global),严格模式下,this始终是指定的值

// demo1function Bar() {	"use strict";	console.log( this.a );}var a = 2;Bar(); // TypeError: `this` is `undefined`  // demo2'use strict';function test() {  console.log(this);};test();// undefined //demo3function foo() {	console.log( this.a );}var a = 2;(function(){	"use strict";	// 调用点在严格模式下而不是函数内容在严格模式下,this指向window	foo(); // 2})();// demo4var color = 'red';function displayColor(){    console.log(this.color);}displayColor.call(null);//redvar color = 'red';function displayColor(){    'use strict';    console.log(this.color);}displayColor.call(null);// TypeError: Cannot read property 'color' of null复制代码

在默认绑定中存在几种容易混淆的情况:

  • 嵌套函数独立调用(this默认绑定到window)
  • IIFE立即执行函数
  • 闭包

嵌套函数独立调用

// Code nestedvar a = 0;var obj = {    a : 2,    foo:function(){        function test(){        //虽然test()函数被嵌套在obj.foo()函数中,但test()函数是独立调用,而不是方法调用。所以this默认绑定到window        console.log(this.a);        }        test();     }}obj.foo();//0复制代码

IIFE(Imdiately Invoked Function Expression)立即执行函数

IIFE有一点的特殊性,但IIFE函数实际上就是函数声明后直接调用执行,这样this绑定就比较清楚了

// 上个例子  Code nested 等价于这个IIFE版本var a = 0;function foo(){    (function test(){        console.log(this.a);    })()};var obj = {    a : 2,    foo:foo}obj.foo();//0复制代码

闭包

var a = 0;function foo(){    function test(){        console.log(this.a);    }    return test;};var obj = {    a : 2,    foo:foo}obj.foo()();// 0 闭包中返回的函数test在这里是独立调用,而不是方法调用,所以this指向window复制代码

闭包中的this默认指向window对象,有时候我们需要在闭包中访问嵌套函数的this,所以我们也常用临时变量 var that = this 缓存外层的this,然后在闭包中使用缓存变量that去访问外层的this


隐式绑定

在隐式绑定中,this的指向可能受「上下文对象」影响,对于函数来讲也就是方法调用。

形如obj.fn,调用点用obj对象来引用函数,this指向obj。看些例子:

function foo() {	console.log( this.a );}var obj1 = {	a: 2,	foo: foo };var obj2 = {	a: 222,    foo: foo};obj1.foo(); // 2obj2.foo(); // 222// 只有对象属性引用链最后一层影响调用点// 结果是42而不是2var obj3 = {	a: 42,	foo: foo};var obj4 = {	a: 2,	obj3: obj3};obj4.obj3.foo(); // 42  谁直接调用,this就指向谁复制代码

注:当调用一个函数时,如果该函数具有上下文对象,this会被绑定到该上下文对象 当调用obj1.foo();this 指向 obj1

首先去判断谁是最直接调用,谁直接调用this就指向谁,如果没有,this指向window,这种方法适用于大部分情况

特殊情况:隐式丢失

隐式丢失是指被隐式绑定的函数丢失绑定对象,从而默认绑定到window。这是一种常见的问题,需要注意判别

隐式丢失一般有以下几种情况:

  • 【函数别名】
  • 【参数传递】
  • 【内置函数】
  • 【间接引用】
  • 【其他情况】

【函数别名】

var a = 0;function foo(){    console.log(this.a);};var obj = {    a : 2,    foo:foo}// 把obj.foo赋予别名bar,造成了隐式丢失// 因为只是把foo()函数赋给了bar,而bar与obj对象则毫无关系,故this指向windowvar bar = obj.foo;bar();//0// 上面的例子等价于var a = 0;var bar = function foo(){    console.log(this.a);}bar();// 0复制代码

【参数传递】

var a = 0;function foo(){    console.log(this.a);};function bar(fn){    fn();}var obj = {    a : 2,    foo:foo}// 把obj.foo当作参数传递给bar函数时,有隐式的函数赋值fn=obj.foo// 与上例类似,只是把foo函数赋给了fn,而fn与obj对象则毫无关系bar(obj.foo); // 0// 上面的例子等价于var a = 0;function bar(fn){    fn();}bar(function foo(){    console.log(this.a);});复制代码

函数作为参数传递,如果想函数执行时保留this指向,使用硬绑定。在上例中,foo.bind(obj)代替obj.foo

【内置函数】

var a = 0;function foo(){    console.log(this.a);};var obj = {    a : 2,    foo:foo}setTimeout(obj.foo,100);// 0// 等价于var a = 0;setTimeout(function foo(){    console.log(this.a);},100);//0复制代码

【间接引用】

函数的"间接引用"很容易在无意间创建,最容易在赋值时发生,会造成隐式丢失

// demo1function foo() {   console.log( this.a );}var a = 2;var o = { a: 3, foo: foo };var p = { a: 4 };o.foo(); // 3// 将o.foo函数赋值给p.foo函数,然后立即执行。// 相当于仅仅是foo()函数的立即执行(p.foo = o.foo)(); // 2// demo2 对比 demo1function bar () {    console.log( this.a );}var a = 2;var o = { a: 3, bar: bar };var p = { a: 4 };o.bar(); // 3// 将o.foo函数赋值给p.foo函数,之后p.foo函数再执行,是属于p对象的foo函数的执行p.bar = o.bar;p.bar();// 4复制代码

【其他情况】

var a = 0;var obj = {    a : 2,    foo:foo};function foo() {    console.log( this.a );};(obj.foo = obj.foo)(); // 0(false || obj.foo)(); // 0(1, obj.foo)(); // 0 知识点:逗号操作符  对它的每个操作数求值(从左到右),并返回最后一个操作数的值。复制代码

判断函数时直接调用,还是通过对象的方法调用就可以判断出this的指向


显式绑定

显式绑定是借助 call(),apply(),bind() 去显式的改变this指向。对于被调用的函数来说,叫做间接调用。

apply、call 的区别

在 JavaScript 中,call 和 apply 都是为了改变某个**函数运行时的上下文(context)**而存在的,简而言之,就是为了改变函数体内部 this 的指向。二者作用完全一样,都是立即执行函数,只是接受的参数形式不太一样:

var func = function(arg1, arg2) {};func.call(this, arg1, arg2, ...)func.apply(this, [arg1, arg2, ...])复制代码

其中 this 是你想指定的上下文,可以是任何一个 JavaScript 对象(JavaScript 中一切皆对象)

call 需要把参数按顺序传递进去。明确知道参数数量时使用

apply 则是把参数放在数组里。不确定参数数量时使用,然后把参数 push 进数组传递进去。当参数数量不确定时,函数内部也可以通过 arguments 这个数组来遍历所有的参数【arguments 已经不推荐使用】。

说到JavaScript中上下文(context),又涉及到几个常用的概念:「定义时上下文」和「运行时上下文」以及「上下文是可以改变的」,这里不深入。

bind

MDN的解释是:方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入bind()方法的第一个参数作为 this,传入bind()方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。

bind() 则是创建一个新的包装函数,并且返回该包装函数,便于稍后调用,而不是立刻执行。

fun.bind(thisArg[, arg1[, arg2[, ...]]])复制代码

注:将null或者undefined作为applycall或者bindthis指定值,回到默认绑定的规则。

由于回到默认绑定规则考虑到污染全局对象,传一个空对象替换null或`undefined,如下:

function foo(a,b){...}// 空对象,也被称为DMZ空对象var ø = Object.create( null );                  foo.apply(ø,[2,3])复制代码

显式绑定中有个特例:硬绑定 在硬绑定中,this的指向不能再被修改

var a = 0;function foo(){    console.log(this.a);}var obj = {    a:2};var bar= function(){    foo.call(obj);}// 在bar函数内部手动调用foo.call(obj)。// 因此,无论之后如何调用函数bar,它总会手动在obj上调用foobar(); // 2setTimeout(bar,100); // 2bar.call(window); // 2复制代码

上面提到到this的硬绑定 和 bind 方法,但硬绑定一旦绑定就无法修改this指向,有一种软绑定的实现方案提供了一种更加灵活的绑定方式

//softBind方法if (!Function.prototype.softBind) {	Function.prototype.softBind = function(obj) {		var fn = this,			curried = [].slice.call( arguments, 1 ),			bound = function bound() {				return fn.apply(					(!this ||						(typeof window !== "undefined" &&							this === window) ||						(typeof global !== "undefined" &&							this === global)					) ? obj : this,					[].concat.call( curried, arguments )				);			};		bound.prototype = Object.create( fn.prototype );		return bound;	};}//使用function foo() {   console.log("name: " + this.name);}var obj = { name: "obj" },    obj2 = { name: "obj2" },    obj3 = { name: "obj3" };var fooOBJ = foo.softBind( obj );fooOBJ(); // name: objobj2.foo = foo.softBind(obj);obj2.foo(); // name: obj2   <---- 看!!!fooOBJ.call( obj3 ); // name: obj3   <---- 看!setTimeout( obj2.foo, 10 ); // name: obj   <---- 退回到软绑定复制代码

此外JavaScript还有很多内置函数,默认具有显示绑定的功能:如:map()、forEach()、filter()、some()、every()

var id = 'oops, global';function foo(el){    console.log(el,this.id);}var obj = {    id: 'fn'};[1,2,3].forEach(foo);// 1 "oops, global" 2 "oops, global" 3 "oops, global"[1,2,3].forEach(foo,obj);// 1 "fn" 2 "fn" 3 "fn"复制代码

new 绑定

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

当使用new调用函数时,会执行以下操作(四个步骤):

  • 创建一个全新对象 :obj
  • 对创建的新对象执行 [[Prototype]] 连接:obj.__proto__ = foo.prototype
  • 绑定this到新创建的新对象上:foo函数内this指向obj
  • 由构造函数返回的对象就是 new 表达式的结果。如果函数没有显式的返回一个对象,那么 new 表达式中的函数调用会自动返回这个创建的新对象(一般情况下,构造函数不返回值,但是用户可以选择主动返回对象,来覆盖正常的对象创建步骤)
// this new绑定demofunction foo(a) {	this.a = a;}var bar = new foo( 2 );console.log( bar.a ); // 2复制代码

综上:this的四种绑定规则:默认绑定、隐式绑定、显式绑定和new绑定,分别对应函数的四种调用方式:独立调用(直接调用)、方法调用、间接调用和构造函数调用。


优先级

「new 绑定」>「显式绑定」> 「隐式绑定」> 「默认绑定」

// Demo1 function foo(){    console.log(this.a);}var obj = {    a:222,    foo,};obj.foo(); // 222;obj.foo.call({
a:1}); // 1 优先级: 显式绑定 > 隐式绑定 // Demo2function func1(a){ this.a = a;}var obj1 = {};var bar = func1.bind(obj1);bar(2);console.log(obj1) // {a:2}var obj2 = new bar(3);console.log(obj1) // {a:2}console.log(obj2) // {a:3} 优先级: new绑定 > 显式绑定 复制代码

ES6 箭头函数 =>

箭头函数没有自己的this, 它的this是继承而来;ES6箭头语法会保存函数创建时的this值,而不是调用时的值。所以this默认指向在定义它时所处的对象(宿主对象),而不是执行时的对象, 定义它的时候,可能环境是window; 箭头函数可以方便地让我们在 setTimeout ,setInterval中方便的使用this。

function foo() {  // 返回一个箭头函数	return (a) => {    // 这里的 `this` 是词法上从 `foo()` 采用的		console.log( this.a );	};}var obj1 = {	a: 2};var obj2 = {	a: 3};var bar = foo.call( obj1 );bar.call( obj2 ); // 2, 不是3!复制代码

箭头函数的本质是词法作用域(和调用点决定的机制不一样)

转载于:https://juejin.im/post/5b6a65e96fb9a04fa5610c02

你可能感兴趣的文章
各大主流虚拟机
查看>>
思科网络技术一览
查看>>
highcharts
查看>>
JavaScript 判断浏览器
查看>>
单位在用MYSQL数据库测试脚本
查看>>
使用jmeter运行java脚本,实现手机号码随机生成
查看>>
std::limits
查看>>
Easel初体验
查看>>
Trying to override old definition of task javac Error
查看>>
Nginx配置文件nginx.conf详解
查看>>
windows7系统远程连接windows server 2003服务器
查看>>
inux乱码问题:LANG变量的秘诀
查看>>
删除有序链表中的重复值 Remove Duplicates from Sorted Array
查看>>
关于系统整合的跑题
查看>>
开发工具,编译器控件
查看>>
Linux 下用 smartd 监测硬盘状况
查看>>
关于编译flex项目的问题
查看>>
redis-cluster创建步骤
查看>>
HT for Web基础动画介绍
查看>>
11g RAC的卸载
查看>>