Javascript 的 this 机制

Javascript 的 this 理解

Javascript 的this是让很多码农(包括自己)觉得复杂的机制,要弄清楚它,就需要弄清楚几个问题:

  • 为什么要用this
  • this 到底是什么?
  • this的绑定规则是什么?
一、为什么要用this

有如下代码:

function showName(){
	return this.name.toUpperCase();
}

function sayHello(){
  var greeting = "Hello,I'm " + showName.call(this);
  console.log(greeting);
}

var xm ={
  name:"xiaoming"
}

var hh={
  name:"huahua"
}

console.log(showName.call(xm)); // XIAOMING
console.log(showName.call(hh)); // HUAHUA

sayHello.call(xm);  // Hello,I'm XIAOMING
sayHello.call(hh);  // Hello,I'm HUAHUA

这段代码可以在不同的上下文对象xmhh(还可以有无数个)中复用函数showName()sayHello(),如果不用this,那就需要给showName()sayHello()显示传入上下文对象:

function showName(context){
	return context.name.toUpperCase();
}

function sayHello(context){
  var greeting = "Hello,I'm " + showName.call(context);
  console.log(greeting);
}

console.log(showName.call(xm));
sayHello.call(hh);

随着调用关系越来越复杂,显示传递上下文对象会让代码变得越来越混乱和难以维护,使用this则能有效避免这个问题。

二、this 到底是什么?

首先说结论,不是指向函数自身,也不是函数作用域,而是运行时绑定的。它的上下文取决于函数调用的各种条件,所以this的绑定只取决于函数的调用方式;当一个函数被调用时,会创建一个运行时上下文:包含函数在那里被调用(调用栈)、函数的调用方式、传入的参数等信息,而this就是这个上下文的一个属性,会在函数执行的过程中用到。

function foo(num){
  console.log('foo被调用了:',num);
  this.count++;
}
foo.count=0;
for(var i=0;i<10;i++){
  if(i>5){
    foo(i)
  }
}
// foo被调用了: 6
// foo被调用了: 7
// foo被调用了: 8
// foo被调用了: 9

// 输出foo总共被调用了多少次
console.log(foo.count); // 0 呐尼?

上面的代码输出证明foo被调用了4次,但是foo.count仍然为0,执行foo.count=0的确向函数对象foo添加了一个属性count,但是函数内部的this并不是指向那个函数对象,所以this.count++不会导致foo.count自增,换个写法(代码只列出了变化的部分):

function foo(num){
  console.log('foo被调用了:',num);
  foo.count++;
}
...

运行一下试试,console.log(foo.count); // 4,再换个写法:

...
for(var i=0;i<10;i++){
  if(i>5){
    foo.call(foo,i)
  }
}
...

运行结果console.log(foo.count); // 4,依然为4,是我们期望的结果,以上证明this不是指向函数自身,再看下面的代码:

function foo(){
  var a=2;
  this.bar();
}
function bar(){
  console.log(this.a);
}
foo(); // a is not defined

这段错误的代码试图通过this联通foo()bar()的词法作用域,从而让bar可以访问foo作用域的变量a,所以this指向作用域是错误说法。

三、this绑定规则

函数的调用位置决定了this的绑定规则,而调用位置则需要从调用栈中查找,它就在当前正在执行的函数的前一个调用中,如下代码:

function baz(){
  // 当前调用栈是:baz
  // 因此,当前的调用位置是全局作用域
  console.log('baz');
  bar(); // <-- bar的调用位置
}
function bar(){
  // 当前调用栈是:baz -> bar
  // 因此,当前的调用位置在baz中
  console.log('bar');
  foo(); // <-- foo的调用位置
}
function foo(){
  // 当前调用栈是:baz -> bar -> foo
  // 因此,当前的调用位置在bar中
  console.log('foo');
}
baz() // <-- baz的调用位置
  • 默认绑定

这条规则是其他绑定规则无法匹配时的默认规则。最常见的独立函数调用,代码如下:

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

声明在全局作用域中的变量(比如var a=2 ),就是全局对象的一个同名属性,所以调用foo()时,this.a被解析成了全局变量a,因为本例应用了默认绑定,this指向全局变量,当然如果是严格模式下,则不能将全局对象用于默认绑定,如下:

function foo(){
  "use strict"
  console.log(this.a);
}
var a = 2;
foo(); // TypeError: this is undefined
  • 隐式绑定

当调用位置有上下文对象或者被某个对象拥有(包含)时,代码如下:

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

严格来说foo()函数不管是在obj中定义还是先定义再添加为引用属性,都是不属于obj对象。然而,调用位置会使用obj上下文来引用函数,因此函数调用时obj对象“拥有”或者“包含”函数引用,隐式绑定规则会把函数调用中的this绑定到这个上下文对象,因为调用foo()this被绑定到obj,因此obj.athis.a是一样的。

对象属性引用链中只有最后一层在调用位置中起作用,代码如下:

function foo(){
  console.log(this.a);
}
var obj1 = {
  a:99,
  foo
}
var obj2 = {
  a:100,
  obj1
}
obj2.obj1.foo(); // 99 this 绑定到最后一层obj1
  • 隐式丢失

隐式绑定一个常见的问题就是隐式绑定的函数会丢失绑定对象,从而绑定到全局对象或者undefined上:

function foo(){
  console.log(this.a);
}
var obj = {
  a:2,
  foo
}
var bar=obj.foo; // 函数别名
var a="oops , global" // a是全局对象的属性
bar(); // oops , global

虽然barobj.foo的一个引用,但实际上引用的是foo()函数本身,因此bar()调用等同于foo()调用,应用默认绑定规则,更常见的是传入回调函数中:

function foo(){
  console.log(this.a);
}
function doFoo(fn){
  // fn 引用的是foo
  fn(); // <-- 调用位置
}
var obj = {
  a:2,
  foo
}
var a="oops , global"
doFoo(obj.foo); // oops , global

参数传递其实是一种隐式赋值,因此传入函数时也会被隐式赋值,索引结果和上一个例子一样,更直观的例子:

function foo(){
  console.log(this.a);
}
function doFoo(fn){
  // fn 引用的是foo
  fn(); // <-- 调用位置
}
var obj = {
  a:2,
  foo
}
var a="oops , global"
setTimeout(obj.foo,100); // oops , global

此处是否想到了箭头函数??

  • 显示绑定

使用call(...)apply(...)方法,可以在某个对象上强制调用函数,它们的第一个参数是一个对象,是给this准备的,称之为显示绑定:

function foo(){
  console.log(this.a);
}
var obj = {
  a:2
}
var a="oops , global"
foo.call(obj); // 2

显示绑定也无法解决绑定丢失的问题,不过一个变种可以解决这个问题:

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

通过一个包裹函数手动调用foo.call(obj),后面无论如何调用函数bar,都会在obj上调用foo,将包裹函数改写为可复用的辅助函数:

function foo(something){
  console.log(this.a,something);
  return this.a + something;
}
// 简单的辅助绑定函数
function bind(fn,obj){
  return function(){
    return fn.apply(obj,arguments);
  }
}
var obj = {
  a:2
}
var bar = bind(foo,obj);
var b = bar(3); // 2 3
console.log(b); // 5

由于这个辅助函数非常常用,所以ES5提供了内置方法Function.prototype.bind,用法如下:

function foo(something){
  console.log(this.a,something);
  return this.a + something;
}
var obj = {
  a:2
}
var bar = foo.bind(obj);
var b = bar(3); // 2 3
console.log(b); // 5

第三方库的许多函数以及Javascript语言和宿主环境中许多的内置函数,都提供一个可选参数,称为上下文(context),其作用和bind一样,确保回调函数使用指定的this

function foo(el){
  console.log(el,':',this.id);
}
var obj = {
  id:"awesom"
};
// 调用foo(...)时把this绑定到obj
[2,3,5].forEach(foo,obj); // 2:awesom  3:awesom  5:awesom

这些函数实际上就是通过call(...)或者apply(...)实现了显示绑定。

  • new绑定

首先要明确一点,jsnew操作符和面向对象语言(java/C#)调用构造函数完全不一样,js的构造函数只是一些使用new操作符时被调用的函数,它们并不属于某个类,也不会实例化一个类,只是被new操作符调用的普通函数,使用new来调用函数,会自动执行下面的操作:

1. 创建一个全新的对象;
2. 这个新对象会被执行`[[Prototype]]`连接;
3. 这个新对象会绑定到函数调用的`this`;
4. 如果函数没有返回其他对象,那么`new`表达式中的函数调用会自动返回这个新对象;
function foo(a){
  this.a=a;
}
var bar = new foo(2);
console.log(bar.a); // 2

使用new来调用foo(...)时,会构造一个新对象并把它绑定到foo(...)调用中的this上,称之为new绑定。

  • 优先级

四条this绑定规则:默认绑定、隐式绑定、显示绑定、new绑定,如果某个调用位置可以应用多条规则该应用哪条规则?首先,默认绑定优先级是最低的,看看隐式绑定和显示绑定:

function foo(){
  console.log(this.a);
}
var obj1={
  a:2,
  foo
}
var obj2={
  a:3,
  foo
}
obj1.foo(); // 2 	隐式绑定
obj2.foo(); // 3	隐式绑定

obj1.foo.call(obj2); // 3  显示绑定
obj2.foo.call(obj1); // 2  显示绑定

可以看到显示绑定优先级更高,那new绑定和隐式绑定呢?

function foo(params){
  this.a = params;
}
var obj1={
  foo
}
var obj2={}
obj1.foo(2);
console.log(obj1.a); // 2

obj1.foo.call(obj2, 3);
console.log(obj2.a); // 3

var bar = new obj1.foo(4);
console.log(obj1.a); // 2
console.log(bar.a); // 4 bar 是new返回的新对象

可以看到new绑定比隐式绑定优先级更高,但new绑定和显示绑定谁的优先级更高呢?因为无法通过new foo.call(obj1)来测试,而Function.prototype.bind(...)创建的包装函数是硬绑定,如下:

function foo(params){
  this.a = params;
}
var obj1={};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); // 2

var baz = new bar(3);
console.log(obj1.a); // 2
console.log(baz.a); // 3

bar被应绑定到obj1上,但是new bar(3)并没有把obj1.a修改为3,相反new修改了绑定到bar(...)中的this,所以new绑定优先级高于硬绑定。

  • 绑定例外

如果把null或者undefined作为this的绑定对象传入callapply或者bind,这些值在调用时会被忽略,实际应用默认绑定:

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

间接应用也会导致调用这个函数引用默认绑定:

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

赋值表达式p.foo = o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo或者o.foo,从而应用默认规则;

  • 箭头函数

ES6中有一种无法应用四种绑定规则的特殊函数:箭头函数。它使用外层(函数或者全局)作用域来决定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

foo()内部创建的箭头函数会捕获调用时foo()this,由于foo()的this绑定到obj1bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改。

结束…~ _ ~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值