深入剖析JavaScript中的this(上)

在Javascript中,this 关键字是一个非常重要的概念,this这个关键字可以说是很常见也用的很多,说它简单也很简单,说它难也很难。我们经常会用到this,也经常会因为this头疼,是一个经常被误解和误用的概念,为什么呢,因为有时候我们不知道this到底指的是什么?怎么用?

在 JavaScript 中,this 的值在函数被调用时确定,而不是在函数被创建时确定。这使得 this 在 JavaScript 中的行为与其他一些语言中的类似关键字(如 Python 的 self 或 Java 的 this)有所不同。本文将从全局作用域或函数外部、普通函数调用、对象的方法、构造函数、事件处理函数、箭头函数几个方面来剖析JavaScript中的this。

一、抛砖引玉

先看一段代码:

var name = "前端技术营";
var obj = {
  name: "张三",
  foo: function() {
    console.log(this.name);
  }
};
var foo = obj.foo;
obj.foo(); // 张三
foo(); // 前端技术营

可以看到上面代码中,obj.foo() 和 foo() 都指向同一个函数,但是执行结果却不一样;产生这种差异的原因,就在于函数体内部使用了 this 关键字。

在《JavaScript高级程序设计》一书中是这样说的,this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this对象通常纸箱window。

所以上面的问题,对obj.foo() 来说, foo 运行在 obj 环境中,所以 this 指向 obj ;对于 foo() 来说, foo 运行在全局环境下,所以在非严格模式下 this 指向 window ,所以导致了两者运行的结果不同。看到这有的人可能就有疑问了,函数的运行环境是如何判定的?为什么 obj.foo() 就是在 obj 环境,为何 var foo = obj.foo; foo() 就在全局环境执行了?插个眼,继续往下看,就明白这个问题了!

二、为什么需要this

先看下面代码:

function foo() {
  console.log(this.name)
}

var bar = {
  name: '张三',
  foo: foo
}
var baz = {
  name: '李四',
  foo: foo
}
bar.foo(); // 张三
baz.foo(); // 李四

Javascript 引擎在处理上面代码时,会在堆内存中,生成两个对象,然后把这两个对象在内存中的地址分别赋值给变量bar和baz。在读取 this.name 时,需要先从变量bar和baz拿到地址,然后再分别从对应地址中拿到对象,再返回它的 name 属性。

对象的属性是一个函数,当引擎遇到对象属性是函数的情况,会将函数单独保存在堆中,然后再将函数的地址赋值给对象属性,而 Javascript 是允许在函数体内引用当前环境的其他变量。那么问题来了,函数可以在不同的运行环境执行,所以我们就需要一种机制,能够在函数内获得当前运行环境,foo只定义了一次,却可以被不同的对象引用,实现了代码共享,由此诞生了 this,它的设计目的就是指向函数运行时所在的环境。

那么,如何正确的在代码中判定this所指向的环境呢?

三、全局作用域中

在全局作用域代码中this 是不变的,this始终是全局对象本身,即window。

var a = '张三';
this.b = '李四';
window.c = '王五';

console.log(this.a); // 张三
console.log(b); // 李四
console.log(this.c); // 王五

console.log(this === window); // true

运行以上代码发现,this === window为true,也就是说在全局作用域中this就是全局对象window,所以上述 a ,b ,c 都相当于在全局对象上添加相应的属性。

var a = 1;
var b = function () {
  return "function1";
}
console.log(window.a); //1
console.log(window.b); //ƒ (){ return "function1"; }
console.log(window.a === a); //true
console.log(window.b === b); //true

在全局对象上定义的变量可以直接访问。

window.aa = 2;
this.bb = function () {
  return "function2";
}
console.log(aa); //2
console.log(bb); //ƒ (){  return "function2"; }

四、函数中的this

在函数中使用this,才是令我们最容易困惑的,这里我们主要是对函数代码中的this进行分析。

这里再次强调一下,this的指向在函数创建的时候是决定不了的,而是在进入当前执行上下文时确定的,也就是在函数执行时并且是执行前确定的。但是同一个函数,作用域中的this指向可能完全不同,但是不管怎样,函数在运行时的this的指向是不变的,而且不能被赋值。

函数中this的指向丰富的多,它可以是全局对象、当前对象、或者是任意对象,当然这取决于函数的调用方式。在JavaScript中函数的调用方式有一下几种方式:作为函数调用、作为对象属性调用、作为构造函数调用、使用apply或call调用。下面我们将按照这几种调用方式一一讨论this的含义。

4.1 作为函数调用

看如下代码:

function foo() {
  var name = '张三';
  console.log(this.name); // undefined
  console.log(this); // Window 
}

foo();

按照我们上面说的this最终指向的是调用它的对象,这里的函数foo实际是被Window对象所点出来的,下面的代码就可以证明。

function foo() {
  var name = '张三';
  console.log(this.name); // undefined
  console.log(this); // Window 
}

window.foo();

和上面代码一样,其实alert也是window的一个属性,也是window点出来的。

function foo() {
  function bar() {
    this.name = '张三';
    console.log(this === window); // true
  }
  bar()
}
foo();
console.log(name); // 张三

上述代码中,在函数内部的函数独立调用,此时this还是被绑定到了window。

在严格模式下,不能将全局对象 window 作为默认绑定,此时 this 会绑定到 undefined ,但是在严格模式下调用函数则不会影响默认绑定。

function foo() {
  "use strict";
  console.log(this===window); // false
  console.log(this===undefined); // true
}
foo();

"use strict"
function foo() {
    var name = "张三";
    console.log(this.name);
};

foo();

// Uncaught TypeError: Cannot read property ‘name’ of undefined at foo
加了"use strict"之后,和上面一样的代码运行就会报错,在严格模式下,不能将全局对象 window 作为默认绑定。

var name = '张三';

function foo() {
  console.log(this.name); // 张三
  console.log(this === window); // true
};

(() => {
  "use strict"
  foo();
})();

看上面代码,在foo() 前加了"use strict",运行并没有报错,依然打印出了结果,可见在严格模式下调用函数则不会影响默认绑定。

小结:当函数作为独立函数被调用时,内部this被默认绑定为(指向)全局对象window,但是在严格模式下会有区别,在严格模式下this被绑定为undefined。

4.2 作为对象属性调用

先看一段代码:

var obj = {
  name: "张三",
  foo: function() {
    console.log(this.name); //张三
  }
}
obj.foo();

根据this最终指向调用它的对象可知,这里的this指向的是对象obj,因为调用这个foo是通过obj.foo()执行的,那自然指向就是对象o。

是不是感觉自己懂了?别急,再看看下边的代码。

var obj = {
  name: "张三",
  foo: function() {
    console.log(this.name); // 张三
  }
}
window.obj.foo();

先解释一下window.obj.foo(),window是js中的全局对象,我们创建的变量实际上是给window添加属性,所以这里可以用window点obj对象。

再看这段代码和上面的那段代码几乎是一样的,但是这里的this为什么不是指向window?如果按照上面的理论,最终this指向的是调用它的对象,那这个理论还成不成立呢,我们先接着再看下面一段代码。

var obj = {
  name: '张三',
  bar: {
    name: '李四',
    foo: function() {
      console.log(this.name); // 李四
    }
  }
}
obj.bar.foo();

这里同样也是对象obj点出来的,但是同样this并没有执行它,那你肯定会说我一开始说的那些不就都是错误的吗?其实也不是,只是一开始说的不准确,接下来补充一句话,我相信你就可以彻底的理解this的指向的问题。

1、如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window,这里需要说明的是在js的严格版中this指向的不是window。

2、如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。

3、如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象。

var obj = {
  name: '张三',
  bar: {
    // name: '李四',
    foo: function() {
      console.log(this.name); // undefined
    }
  }
}
obj.bar.foo();

这段代码尽管对象bar中没有属性name,这个this指向的也是对象bar,因为this只会指向它的上一级对象,不管这个对象中有没有this要的属性。

var obj = {
  name: '张三',
  bar: {
    age: 18,
    foo: function() {
      console.log(this.age); // undefined
      console.log(this); // window
    }
  }
}

var fn = obj.bar.foo;
fn();

解释:obj.bar.foo方法声明部分,只需要理解为在堆内存中开辟了一块空间,并由obj.bar.foo持有这块内存空间的引用,由于函数尚未执行,因此还没有确定this。将obj.foo赋值给bar,也就是将函数的引用拷贝一份给了bar,bar独立调用,因此this指向window。this永远指向的是最后调用它的对象,也就是看它执行的时候是谁调用的。

4.3 使用apply或call调用

apply和call为函数原型上的方法。它可以更改函数内部this的指向。

var name = '前端技术营';

function foo() {
  console.log(this.name);
}
var obj1 = {
  name: '张三'
}
var obj2 = {
  name: '李四'
}
var obj3 = {
  name: '王五'
}
// this指向window,打印“前端技术营”
foo();
// this指向 obj1,打印“张三”
foo.apply(obj1);
// this指向 obj2,打印“李四”
foo.call(obj2);
// this指向 obj3,打印“王五”
foo.call(obj3);

当函数foo 作为独立函数调用时,this被绑定到了全局对象window,当使用bind、call或者apply方法调用时,this 被分别绑定到了不同的对象。

call和apply 的功能一样,唯一不同的是传给函数的参数的方式,第一个参数是this指向的新对象,从第二个参数开始,apply传数组,这个数组包含函数所需要的参数,apply只支持传入一个数组,哪怕是一个参数也要是数组形式,最终调用函数时候这个数组会拆分成一个个参数分别传入;call 直接传参数,多个参数逗号分割。

var obj1 = {
  name: '张三'
}
var obj2 = {
  name: '李四'
}

function foo(arg1, arg2) {
  console.log(this);
  console.log(arg1 + arg2);
};

foo.call(obj1, 1, 2); 
// {name: '张三'}
// 3

foo.apply(obj2, [1, 2]); 
// {name: '李四'}
// 3

还有一个bind方法。bind方法和call使用方式一样,作用也一样,不一样的是实现方式,call和apply传参结束后直接执行函数,而bind只是更改this值和给函数传参,函数并不执行,所以bind可以作为事件的处理函数去使用。

var name = '前端技术营';
  
function foo() {
  console.log(this.name);
}
var obj = {
  name: '张三'
}

foo.bind(obj);
console.log(foo.bind(obj)) 
// ƒ foo() { console.log(this.name); }

foo.bind(obj)(); // 张三
function add(a, b){
  return a + b
}
function sub(a, b){
  return a - b
}
add.bind(sub, 5, 3)(); // 8

4.4 作为构造函数调用

var name = '前端技术营';
  
function Foo(){
  this.name = "张三";
}
var baz = new Foo();
console.log(baz.name); // 张三

这里之所以对象baz可以点出函数Foo里面的name是因为new关键字可以改变this的指向,将这个this指向对象baz(因为用了new关键字就是创建一个对象实例),这里用变量baz创建了一个Foo的实例(相当于复制了一份Foo到对象baz里面),此时仅仅只是创建,并没有执行,而调用这个函数Foo的是对象baz,那么this指向的自然是对象baz。那么为什么对象baz中会有name,因为你已经复制了一份Foo函数到对象baz中,用了new关键字就等同于复制了一份。

4.5 总结

当我们要判断当前函数内部的this绑定,可以依照下面的原则:

(1)函数是否在是通过 new 操作符调用?如果是,this 绑定为新创建的对象。

var bar = new foo(); // this指向bar
(2)函数是否通过call或者apply调用?如果是,this 绑定为指定的对象

foo.call(obj1); // this指向obj1
foo.apply(obj2); // this指向obj2
(3)函数是否通过 对象 . 方法调用?如果是,this 绑定为当前对象

obj.foo(); // this指向obj
(4)函数是否独立调用?如果是,this 绑定为全局对象。

foo(); // this指向window

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值