javascript中的关键字this


前言

面向对象语言中 this 表示当前对象的一个引用。
但在 JavaScript 中 this 不是固定不变的,它会随着执行环境的改变而改变。
1、在对象方法中, this 指向调用它所在方法的对象。
2、单独使用 this,它指向全局(Global)对象。
3、函数使用中,this 指向函数的所属者。
4、严格模式下函数是没有绑定到 this 上,这时候 this 是 undefined。
5、在 HTML 事件中,this 指向了接收事件的 HTML 元素。
6、apply 和 call 允许切换函数执行的上下文环境(context),即 this 绑定的对象,call() 和 apply() 方法可以将 this 引用到任何对象。


提示:以下是本篇文章内容,案例仅供参考

一、ES6 箭头函数中的 this?

许多人,都会这么描述箭头函数里 this 的行为:局部的 this。什么意思呢?是否局部(Lexical)?

<div id="demo" onclick="foo()" > 点我</div>
<script>
    document.getElementById('demo').onclick =
	function(){
        console.log(this.tagName);      //div
        setTimeout(function(){
            console.log(this);          //window
        },1000)
    }
</script>
function foo() {
   setTimeout( () => {
      console.log("id:", this.id);
   },100);
}
foo.call( { id: 42 } );      // id: 42

这里的 => (箭头函数)看起来把它内部的 this 绑定为父函数 foo() 里的 this。如果这个内部函数是一个常规的函数(声明或表达式),它的 this 将类似 setTimeout 如何调用函数一样被控制着。
局部变量 this

一个描述 this 行为观察的常用伎俩是:

function foo() {
   var self = this;
   setTimeout(function() {
      console.log("id:", self.id);
   },100);
}

foo.call( { id: 42 } );
// id: 42

旁注:上方“self”的变量名其实是一个非常糟糕、容易误解的名字,它意味着把 this 指向函数自己,而它并没有这么做。

var that = this 也是一个同样不妥的语义,特别当存在多个作用域而使用(that1, that2, …)的时候更糟糕。如果你想起个语义妥当的好名字,可以试试 var context = this,因为它能准确描述 this 是什么——一个动态的上下文。

从上方的代码段我们可以看到,我们并没有在内部函数中使用到 this,取而代之的是一个更具预见性的局部变量。我们在外部函数中声明了变量 self,简单地关联了内部函数里用到的变量。

这么一来我们通过使用局部作用域以及闭包的原理,彻底地绕过方程式(示例代码中的内部函数)中绑定 this 的规则。

我们会(错误地)认为 => 箭头函数有着一个跟局部变量/闭包机制一样的“局部 this”行为。

但这种观点并不正确,坑爹了。

箭头函数的this绑定

咱可通过另一个方法来观察箭头函数中 this 的行为——给内部函数做一个强制绑定:

function foo() {
   setTimeout(function() {
      console.log("id:", this.id);
   }.bind(this),100);
}
foo.call( { id: 42 } );          // id: 42

你可以看到我们使用了 .bind(this) 来把内部函数中的 this 绑定到了外部函数去,这样一来无论 setTimeout 会选择如何调用赋予它的函数,该函数都会使用 foo() 里所使用到的 this。

是的,这个版本的代码中我们观测到的行为跟之前两段示例代码所要论述的一样,它更准确么?许多童鞋都认为 => 箭头函数就是这么工作的。

啧啧图样图森破了

生来局部

TC39的常客 Dave Herman 曾更仔细、准确地向我阐述过这个问题,但我很愧疚一直没能完全了解他所陈述的含义,因此对于我往日不准确的言论我就更感歉意了,也更能接纳他人的观点。

Dave 主要对我这么说,“你提及的’局部 this’的描述很蹩脚,因为 this 无论如何都是局部的”。

真的么?嗯哼~

他继续说道,“箭头函数 => 所改变的并非把 this 局部化,而是完全不把 this 绑定到里面去”。

等等,这样合理么?我明明可以在 => 箭头函数里使用 this 的不是么?

当然可以,不过一切是这么发生的 —— 虽然 => 箭头函数没有一个自己的 this,但当你在内部使用了 this,常规的局部作用域准则就起作用了,它会指向最近一层作用域内的 this。

来个示例:

function foo() {
   return () => {
      return () => {
         return () => {
            console.log("id:", this.id);
         };
      };
   };
}

foo.call( { id: 42 } )()()();         // id: 42

思考下,在这段代码中,

有多少次 this 的绑定执行了呢?大部分人会认为有4次——每个函数里各一次。

事实上更准确地说,只有一次才对,它发生于 foo() 函数中。

这些接连内嵌的函数们都没有声明它们自己的 this,所以 this.id 的引用会简单地顺着作用域链查找,一直查到 foo() 函数,它是第一处能找到一个确切存在的 this 的地方。

说白了跟其它局部变量的常规处理是一致的!this 生来局部,而且一直都保持局部态。=>箭头函数并不会绑定一个 this 变量,它的作用域会如同寻常所做的一样一层层地去往上查找。

不仅仅是this

如果你贸贸然地同意了“箭头函数就是常规function的语法糖”这样的观点,那是不正确的,因为事实并非如此——箭头函数里并不按常规支持 var self = this 或者 .bind(this) 这样的糖果。

那些错误的解释都是典型的“给对了答案却讲错了原因”,就像你在高中代数课的测试上明明写对了答案,但老师仍会画圈圈告诉你用错方法了——如何解得答案才是最重要的!

另外,关于“=>箭头函数不绑定自身的 this,而允许局部作用域的方案来沿袭处理之”的正确描述,也解释了箭头函数的另一个情况——它们在函数内部不走寻常路的孩子不仅仅是 this。

事实上 =>箭头函数并不绑定 this,arguments,super(ES6),抑或 new.target(ES6)。

这是真的,对于上述的四个(未来可能有更多)地方,箭头函数不会绑定那些局部变量,所有涉及它们的引用,都会沿袭向上查找外层作用域链的方案来处理。

思考下这段代码:

function foo() {
   setTimeout( () => {
      console.log("args:", arguments);
   },100);
}
foo( 2, 4, 6, 8 );
// args: [2, 4, 6, 8]

这段代码中,=>箭头函数并没有绑定 arguments,所以它会以 foo() 的 arguments 来取而代之,而 super 和 new.target 也是一样的情况。

总结

不要不经思考就轻易接受那些不准确的答案,不用满足于那些通过错误形式获取到的正确答案。

这关系到了事物是怎样运作的,以及你使用了怎样的心智模型(mental model),你会使用这种心智模型去分析、描述和调试其它的行为,如果你在一开始的时候就偏离了轨道,那么在之后你也只会一直停留在错误的轨道上。

二、什么情况下不能用箭头函数

1.定义对象方法2.定义原型方法
3.定义构造函数4.定义事件回调函数

1. 定义对象方法

JS 中对象方法的定义方式是在对象上定义一个指向函数的属性,当方法被调用的时候,方法内的 this 就会指向方法所属的对象。

1.1 定义字面量方法

因为箭头函数的语法很简洁,可能不少同学会忍不住用它来定义字面量方法,比如下面的例子 JS Bin:

const calculator = {
     array: [1, 2, 3],
     sum: () => {
         console.log(this === window); // => true
         return this.array.reduce((result, item) => result + item);
     }
 };
 console.log(this === window); // => true
 // Throws "TypeError: Cannot read property 'reduce' of undefined"
 calculator.sum();

calculator.sum 使用箭头函数来定义,但是调用的时候会抛出 TypeError,因为运行时 this.array 是未定义的,调用 calculator.sum 的时候,执行上下文里面的 this 仍然指向的是 window,原因是箭头函数把函数上下文绑定到了 window 上,this.array 等价于 window.array,显然后者是未定义的。

解决的办法是,使用函数表达式或者方法简写(ES6 中已经支持)来定义方法,这样能确保 this 是在运行时是由包含它的上下文决定的,修正后的代码如下 JS Bin:

const calculator = {
        array: [1, 2, 3],
        sum() {
            console.log(this === calculator); // => true
            return this.array.reduce(
            (result, item) => result + item);
        }
    };
    calculator.sum(); // => 6

这样 calculator.sum 就变成了普通函数,执行时 this 就指向 calculator 对象,自然能得到正确的计算结果。

2 定义原型方法

同样的规则适用于原型方法(prototype method)的定义,使用箭头函数会导致运行时的执行上下文错误,比如下面的例子 JS Bin:

function Cat(name) {
    this.name = name;
}
Cat.prototype.sayCatName = () => {
    console.log(this === window); // => true
    return this.name;
}; 
const cat = new Cat('Mew');
cat.sayCatName(); // => undefined

使用传统的函数表达式就能解决问题 JS Bin:

function Cat(name) {
    this.name = name;
}
Cat.prototype.sayCatName = function () {
    console.log(this === cat); // => true
    return this.name;
};
const cat = new Cat('Mew');
cat.sayCatName(); // => 'Mew'

sayCatName 变成普通函数之后,被调用时的执行上下文就会指向新创建的 cat 实例。

3. 定义构造函数

构造函数中的 this 指向新创建的对象,当执行 new Car() 的时候,构造函数 Car 的上下文就是新创建的对象,也就是说 this instanceof Car === true。显然,箭头函数是不能用来做构造函数, 实际上 JS 会禁止你这么做,如果你这么做了,它就会抛出异常。
换句话说,箭头构造函数的执行并没有任何意义,并且是有歧义的。比如,当我们运行下面的代码 JS Bin:

const Message = (text) => {
    this.text = text;
};
// Throws "TypeError: Message is not a constructor"
const helloMessage = new Message('Hello World!');

构造新的 Message 实例时,JS 引擎抛了错误,因为 Message 不是构造函数。在笔者看来,相比旧的 JS 引擎在出错时悄悄失败的设计,ES6 在出错时给出具体错误消息是非常不错的实践。可以通过使用函数表达式或者函数声明 来声明构造函数修复上面的例子 JS Bin:

const Message = function(text) {
    this.text = text;
};
const helloMessage = new Message('Hello World!');
console.log(helloMessage.text); // => 'Hello World!'

4. 定义事件回调函数

this 是 JS 中很强大的特性,可以通过多种方式改变函数执行上下文,JS 内部也有几种不同的默认上下文指向,但普适的规则是在谁上面调用函数 this 就指向谁,这样代码理解起来也很自然,读起来就像在说,某个对象上正在发生某件事情。
但是,箭头函数在声明的时候就绑定了执行上下文,要动态改变上下文是不可能的,在需要动态上下文的时候它的弊端就凸显出来。比如在客户端编程中常见的 DOM 事件回调函数(event listenner)绑定,触发回调函数时 this 指向当前发生事件的 DOM 节点,而动态上下文这个时候就非常有用,比如下面这段代码试图使用箭头函数来作事件回调函数 JS Bin:

const button = document.getElementById('myButton');
button.addEventListener('click', () => {
    console.log(this === window); // => true
    this.innerHTML = 'Clicked button';
});

在全局上下文下定义的箭头函数执行时 this 会指向 window,当单击事件发生时,浏览器会尝试用 button 作为上下文来执行事件回调函数,但是箭头函数预定义的上下文是不能被修改的,这样 this.innerHTML 就等价于 window.innerHTML,而后者是没有任何意义的。
使用函数表达式就可以在运行时动态的改变 this,修正后的代码 JS Bin:

const button = document.getElementById('myButton');
 button.addEventListener('click', function() {
     console.log(this === button); // => true
     this.innerHTML = 'Clicked button';
 });

当用户单击按钮时,事件回调函数中的 this 实际指向 button,这样的 this.innerHTML = ‘Clicked button’ 就能按照预期修改按钮中的文字。

vue中不能用箭头函数的情况
1.不应该使用箭头函数来定义一个生命周期方法
2.不应该使用箭头函数来定义 method 函数
3.不应该使用箭头函数来定义计算属性函数
4.不应该对 data 属性使用箭头函数
5.不应该使用箭头函数来定义 watcher 函数

原因:
箭头函数绑定了父级作用域的上下文,this 将不会按照期望指向 Vue 实例。
也就是说,你不能使用this来访问你组件中的data数据以及method方法了。
this将会指向undefined。


三、深入理解this的4种绑定规则

深入理解this的绑定规则

如果要问javascript中哪两个知识点容易混淆,作用域查询和this机制绝对名列前茅。本文谈谈—this的4种绑定规则

默认绑定

全局环境中,this默认绑定到window

console.log(this === window);//true
 //函数独立调用时,this默认绑定到window
function foo(){
  console.log(this === window);
}
foo(); //true

被嵌套的函数独立调用时,this默认绑定到window

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

//虽然test()函数被嵌套在obj.foo()函数中,但test()函数是独立调用,而不是方法调用。所以this默认绑定到window

自执行函数【IIFE】

IIFE立即执行函数实际上函数声明后直接调用执行

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

//等价于上例

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

【闭包】

类似地,test()函数是独立调用,而不是方法调用,所以this默认绑定到window

[注意]函数共有4种调用方式,函数调用相关内容移步至此

 var a = 0;
   function foo(){
    function test(){
	    console.log(this.a);
    }
    return test;
   };
   var obj = {
    a : 2,
    foo:foo
   }
   obj.foo()();//0

由于闭包的this默认绑定到window对象,但又常常需要访问嵌套函数的this,
所以常常在嵌套函数中使用var that = this,然后在闭包中使用that替代this,
使用作用域查找的方法来找到嵌套函数的this值

 var a = 0;
  function foo(){
   var that = this;
   function test(){
    console.log(that.a);
   }
   return test;
  };
  var obj = {
   a : 2,
   foo:foo
  }
  obj.foo()();//2

隐式绑定
  一般地,被直接对象所包含的函数调用时,也称为方法调用,this隐式绑定到该直接对象

  function foo(){
   console.log(this.a);
  };
  var obj1 = {
   a:1,
   foo:foo,
   obj2:{
   a:2,
    foo:foo
   }
  }

//foo()函数的直接对象是obj1,this隐式绑定到obj1
obj1.foo();//1

//foo()函数的直接对象是obj2,this隐式绑定到obj2
obj1.obj2.foo();//2

隐式丢失

隐式丢失是指被隐式绑定的函数丢失绑定对象,从而默认绑定到window。这种情况容易出错却又常见

【函数别名】

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

//把obj.foo赋予别名bar,造成了隐式丢失,因为只是把foo()函数赋给了bar,而bar与obj对象则毫无关系

var 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);
 });

【内置函数】

内置函数与上例类似,也会造成隐式丢失

 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

【间接引用】

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

  function 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

function 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函数,之后p.foo函数再执行,是属于p对象的foo函数的执行
p.foo = o.foo;
p.foo();//4

【其他情况】

在javascript引擎内部,obj和obj.foo储存在两个内存地址,简称为M1和M2。只有obj.foo()这样调用时,是从M1调用M2,因此this指向obj。但是,下面三种情况,都是直接取出M2进行运算,然后就在全局环境执行运算结果(还是M2),因此this指向全局环境

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

显式绑定

通过call()、apply()、bind()方法把对象绑定到this上,叫做显式绑定。对于被调用的函数来说,叫做间接调用

var a = 0;
function foo(){
	console.log(this.a);
}
var obj = {
a:2
};
foo();//0
foo.call(obj);//2

普通的显式绑定无法解决隐式丢失问题

var a = 0;
function foo(){
	console.log(this.a);
}
var obj1 = {
	a:1
};
var obj2 = {
	a:2
};
foo.call(obj1);//1
foo.call(obj2);//2

【硬绑定】

硬绑定是显式绑定的一个变种,使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上调用foo
bar();//2
setTimeout(bar,100);//2
bar.call(window);//2

【API】

javascript中新增了许多内置函数,具有显式绑定的功能,如数组的5个迭代方法:map()、forEach()、filter()、some()、every()

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

new绑定
  如果函数或者方法调用之前带有关键字new,它就构成构造函数调用。对于this绑定来说,称为new绑定
【1】构造函数通常不使用return关键字,它们通常初始化新对象,
当构造函数的函数体执行完毕时,它会显式返回。
在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值

function fn(){
this.a = 2;
}
var test = new fn();
console.log(test);//{a:2}

【2】如果构造函数使用return语句但没有指定返回值,或者返回一个原始值,
  那么这时将忽略返回值,同时使用这个新对象作为调用结果

function fn(){
	this.a = 2;
	return;
}
var test = new fn();
console.log(test);//{a:2}

【3】如果构造函数显式地使用return语句返回一个对象,那么调用表达式的值就是这个对象

var obj = {a:1};
function fn(){
	this.a = 2;
	return obj;
}
var test = new fn();
console.log(test);//{a:1}

[注意]尽管有时候构造函数看起来像一个方法调用,它依然会使用这个新对象作为this。
  也就是说,在表达式new o.m()中,this并不是o

var o = {
m: function(){
	return this;
}
}
var obj = new o.m();
console.log(obj,obj === o);//{} false
console.log(obj.constructor === o.m);//true

严格模式
  【1】严格模式下,独立调用的函数的this指向undefined

function fn(){
'use strict';
	console.log(this);//undefined
}
fn();

function fn(){
	console.log(this);//window
}
fn();

【2】在非严格模式下,使用函数的call()或apply()方法时,null或undefined值会被转换为全局对象。而在严格模式下,函数的this值始终是指定的值

var color = 'red';
function displayColor(){
	console.log(this.color);
}
displayColor.call(null);//red

var color = 'red';
function displayColor(){
'use strict';
	console.log(this.color);
}
displayColor.call(null);//TypeError: Cannot read property 'color' of null

总结

提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,

最后

  • - this的四种绑定规则:
    • 默认绑定
      • 调用方式:独立调用
    • 隐式绑定
      • 调用方式:方法调用
    • 显式绑定
      • 调用方式:间接调用。
    • new绑定
      • 调用方式:构造函数调用。
  1. 分清这四种绑定规则不算难,比较麻烦的是识别出隐式丢失的情况
  2. 说到底,javascript如此复杂的原因是因为函数过于强大。因为,函数是对象,所以原型链比较复杂;
  3. 因为函数可以作为值被传递,所以执行环境栈比较复杂;同样地,因为函数具有多种调用方式,所以this的绑定规则也比较复杂
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值