JavaScript中的this关键词
this是我们在JS经常使用到的关键字之一。经常在面试的时候,会被问到this代表的具体含义,今天也本着学习的目的,仔细整理this的知识点。
引子:this指向它的调用者
我们可以看一个例子
var obj = {a: '我是obj',foo:function(){console.log(this.a)}}var a = '我是window'var foo = obj.fooobj.foo() // 我是objfoo() // 我是window
this指向他的调用者,其调用者是函数运行时所在的环境。两个foo的运行环境是不同的,obj.foo的运行环境是obj,第二个是全局环境window,所以两者的结果不一样。
为什么产生这种差异,这就需要了解内存的存储方式
函数的存储
函数是一个复杂数据类型,在js中是以对象的形式存在,区别与基本数据类型,是一种引用数据类型,函数的声明创建需要在内存堆(heap)中开辟出一块地址,再将内存地址赋值给变量。
比如
var obj = { foo: 5 }

这样理解:代码将一个对象赋值给变量obj,JS会现在内存里面生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj。
也就是说obj是一个地址,后面读取obj的过程是:JS会先获取到obj中指向的地址,再从该地址读出原始的对象,返回它的foo属性。
原始的对象在堆中都是以字典结构保存数据,每一个属性名对应一个属性面熟对象。上述的foo实际上是按照如图的形式保存
函数的调用
当堆中foo的value指向一个函数的地址,如下:
我们可以看见在JS中,函数会单独的保存在内存中,然后采用引用地址的方式赋值。
为什么要反复的说函数的调用、执行,因为函数的调用和它所在的执行环境(执行上下文[Execution Context])有关。函数的调用影响了this的指向问题。
那么在JS中有多少种函数的调用:
直接调用, 包括函数内部的嵌套调用,递归调用
function foo() {}foo()
作为对象的方法调用
var obj = {value : 0,increment : function (inc) {this.value += typeof inc === 'number' ? inc :1;}}obj.increment();console.log(obj.value); //1obj.increment(2);console.log(obj.value); //2
使用 call 和 apply 动态调用
call 和apply是Function的原型方法,它们能够将特定函数当做一个方法绑定到指定对象上,并进行调用
function.call(thisobj, args...)function.apply(thisobj, [args])
function表示要调用的函数;
参数thisobj表示绑定对象,即this指代的对象;
参数args表示要传递给被调用函数的参数。call方法可以接收多个参数列表,而apply只能接收一个数组或者伪类数组,数组元素将作为参数列表传递给被调用的函数。
new 命令间接调用
使用new命令可以实例化对象,这是它的主要功能,但是在创建对象的过程中会激活并运行函数。因此,使用new命令可以间接调用函数。
function f(x,y) { //定义函数console.log("x =" + x + ", y =" + y);}new f(3,4);
使用new命令调用函数时,返回的是对象,而不是return的返回值。如果不需要返回值,或者return的返回值是对象,则可以选用new间接调用函数。
当我们了解了函数的执行方式再去分析this就会方便一些。因为函数的执行上下文决定了this的指向问题。
this
引入
在JavaScript中允许在函数体内部,引用当前环境的其他变量。
var f = function () {console.log(x);};
上面代码中,函数体里面使用了变量x。该变量就是由行运环境提供。
现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。这时,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。
定义
顺带说一下this在严格模式和非严格模式对于全局环境的定义稍微有不同。在非严格模式指向window,在严格模式指向undefined。
全局环境
无论是否在严格模式下,在全局执行环境中(在任何函数体外部this都指向全局对象。在全局执行环境中使用this,表示Global对象,在浏览器中就是window。
console.log(this); //Windowconsole.log(typeof this); //objectconsole.log(this === window); //true
函数(运行内)环境
函数内部直接出现this
在函数执行环境中使用this时,如果函数没有明显的作为非window对象的属性,而只是定义了函数,不管这个函数是不是定义在另一个函数中,这个函数中的this仍然表示window
// "use strict"function A(){//在A函数中定义一个B函数function B(){console.log(this); //Windowconsole.log(typeof this); //objectconsole.log(this === window); //true}//在A函数内部调用B函数B();}//调用A函数A();
当然上述的结果是有待商榷的,因为一般地,在非严格模式下是window,在严格模式下,this将保持他进入执行环境时的值,所以此时this将会是默认的undefined
在严格模式中,
this在上述的环境中确实为undefined,有一些浏览器最初在支持严格模式时没有正确实现这个功能,于是它们错误地返回了window对象
那么,当我们想把this的值从一个环境传递到另一个环境,就可以使用call和apply方法
// 将一个对象作为call和apply的第一个参数,this会被绑定到这个对象。var obj = { a: 'Custom' };// 这个属性是在global对象定义的。var a = 'Global';function whatsThis(arg) {return this.a; // this的值取决于函数的调用方式,默认为undefined[严格]}whatsThis(); // 'Global'whatsThis.call(obj); // 'Custom' this->objwhatsThis.apply(obj); // 'Custom' this->obj
当一个函数在其主体中使用this关键字时,可以通过使用函数继承自Function.prototype的call或apply方法将this值绑定到调用中的特定对象。
function add(c, d) {return this.a + this.b + c + d;}var o = {a: 1, b: 3};// 第一个参数是作为‘this’使用的对象// 后续参数作为参数传递给函数调用add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16// 第一个参数也是作为‘this’使用的对象// 第二个参数是一个数组,数组里的元素用作函数调用中的参数add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34
使用call和apply函数的时候要注意,如果传递给this的值不是一个对象,JavaScript会尝试使用内部ToObject操作将其转换为对象。因此,如果传递的值是一个原始值比如7或foo,那么就会使用相关构造函数将它转换为对象,所以原始值7会被转换为对象,像new Number(7)这样,而字符串foo转化成 new String('foo')这样,例如:
function bar() {console.log(Object.prototype.toString.call(this));}//原始值 7 被隐式转换为对象bar.call(7); // [object Number]
作为对象的方法
当函数作为对象里的方法被调用时,它们的this是调用该函数的对象。
//定义一个对象obj,并为它添加属性name,添加方法objFunvar obj = {name: 'jack',objFun: function(){console.log(this); // Object {name: "jack"}console.log(typeof this); //objectconsole.log(this === window); //falseconsole.log(this.name); //jack}};//调用obj对象的方法obj.objFun(); //this 绑定到当前对象,也就是obj对象
请注意,这样的行为,根本不受函数定义方式或位置的影响。在前面的例子中,我们在定义对象obj的同时,将函数内联定义为成员objFun 。但是,我们也可以先定义函数,然后再将其附属到obj.objFun。这样做会导致相同的行为:
var obj ={ name: 'jack' }function objFun() {console.log(this);console.log(typeof this);console.log(this === window);console.log(this.name);}obj.objFun = objFunobj.objFun();// Object {name: "jack"}// object// false// jack
这表明 函数是从obj的objFun成员调用 才是重点。
当我们简单的修改一下:
//定义一个对象obj,并为它添加属性name,添加方法objFunvar obj = {name: 'jack',objFun: function(){console.log(this); // windowconsole.log(typeof this); //objectconsole.log(this === window); //trueconsole.log(this.name); //undefined}};var test = obj.objFun;test(); // 等价于window.test()
这时候,你会神奇的发现,上面例子中的this又等于window 了,这也是常见的this丢失问题,到底是什么原因导致的呢?下面我们来分析分析:
在绝大多数情况下,函数的调用方式决定了this的值,this不能在执行期间被赋值,并且在每次函数被调用时this的值也可能会不同。每次你调用一个函数this总是重新求值(但这一过程发生在函数代码实际执行之前),函数内部的this值实际上是由函数被调用的父作用域提供,更重要的是,它依赖实际函数的语法。在这里我们可以明显的知道test()等价于window.test(),window给test提供了父作用域(执行环境)。
this的绑定只受最靠近的成员引用的影响。
原型链中的this,getter与setter中的this都是同样适用函数调用者这一个概念
bind方法
ES5引入了bind方法来设置函数的this值,而不用考虑函数如何被调用的。说白了,调用bind方法会创建一个与原函数具有相同函数体和作用域的函数,但是在这个新函数中,this将永久地被绑定到bind中第一个参数上面,无论后生成的函数是如何调用的(只生效一次)。
function cat() {return this.catName}const cat1 = cat.bind({ catName: 'lucy' })console.log(cat1()) // lucyconst cat2 = cat.bind({ catName: 'mimi' })console.log(cat2()) // mimi/* 这个里面bind只生效一次 */
箭头函数
在ES6中,箭头函数出现的作用不仅仅是让函数的书写变得很简洁,可读性很好之外,最大的优点就是解决了this执行环境所造成的的一些问题。比如:匿名函数this指向问题(匿名函数的执行环境具有全局性),以及setTimeout和setInterval中使用this所造成的问题。
在箭头函数中,this与封闭词法环境的this保持一致。在全局代码中,它将被设置为全局对象:
var globalObject = this;var foo = (() => this);console.log(foo() === globalObject); // true
上述的描述比较专业,可以变化的稍微好理解一点
箭头函数不会创建自己的this,它只会从自己的作用域链的上一层’继承’this
const obj = {name: 'jack',a: function() {console.log(this.name) // jackwindow.setTimeout(() => {console.log(this.name) // jack})}}obj.a.call(obj) //第一个this是obj对象,第二个this还是obj对象
注:函数obj.a没有使用箭头函数,因为它的this还是obj,而setTimeout里的函数使用了箭头函数,所以它会和作用域链的上一层的this保持一致,也是obj;如果setTimeout里的函数没有使用箭头函数,那么它打出来的应该是window对象(非严格模式)。
在箭头函数中,如果将this传递给call、bind、或者apply,它将被忽略。不过你仍然可以为调用添加参数,不过第一个参数thisArg应该设置为null。
const obj = {name: 'jack',a: () => {console.log(this.name)}}obj.a() //打出来的是windowobj.a.call('123') //打出来的结果依然是window对象
为什么总是window对象,或者说为什么会被忽略呢?
因为箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this
我们可以做一个普通函数和箭头函数的比较:
const obj = {name: 'jack',a: function() { console.log(this.name) },b: {name: 'lucy',c: function() { console.log(this.name) }}}obj.a() // 打印:jack,相当于obj.a.call(obj)obj.b.c() // 打印:lucy,相当于obj.b.call(obj.b)
const obj = {name: 'jack',a: function() { console.log(this.name) },b: {name: 'lucy',c: () => { console.log(this.name) }}}obj.a() //没有使用箭头函数打出的是jackobj.b.c() //打出的是window对象(非严格模式)!
打出window对象的原因,是window对象就是它的上一层this,而你的多层嵌套只是对象嵌套,这时候没有作用域链的嵌套,实际上对箭头函数来说,还是只有自己一级的作用域,和上一层的window作用域
构造函数方式(new)
当一个函数用作构造函数时(适用new关键词),他的this被绑定到正在构造的新对象(实例对象)。
我们需要简单的了解new操作符在调用构造函数的时候都发生了什么
构造函数和普通函数比较重要的两个区别:
- 构造函数首字母一般大写,约定俗成,便于区分;
- 构造函数的调用使用
new操作符,普通函数的调用有很多方式,但是都用不到new。
使用new操作符创建对象是发生的事情:
- 创建一个
Object对象实例。 - 将构造函数中的执行对象赋值给新生成的这个实例。
- 执行构造函数中的代码。
- 返回新生成的对象实例。
原本构造函数是window对象的方法,默认作用域是全局作用域,如果不用new操作符而直接调用,那么构造函数的执行对象就是window,即this指向了window。现在用new操作符后,this就指向了新生成的对象。执行构造函数中的代码,如下:
function Dog() {this.name = 'Tony'}function Cat() {this.name = 'Mimi'/* 如果函数具有返回对象的return语句,则该对象是new表达式的结果。否则表达式的结果就是当前绑定的this的对象,我们从打印上面看this.name = 'Mimi'好像没执行,其实是执行了,只是对外部没有产生任何影响,如果我们将其移动到return的下面,还是能够看到改变的,只是忽略了它的含义 */return { word: 'I am mimi.' }}var dog = new Dog()console.log(dog.name) // Tonyvar cat = new Cat()console.log(cat.name) // I am mimi
虽然构造函数返回的默认值是this所指的那个对象,但它仍可以手动返回其他的对象(如果返回值不是一个对象,则返回this对象)。
总结
还有一些比较少见的用途,这里就不枚举了。总的来说,this这个关键词清楚了调用它的函数的执行环境再去理解this本身就会轻松许多!
现在面试的时候让你谈一谈this就不会1分钟解决战斗了吧!
【参考资料】
