JavaScript中的this关键词
this
是我们在JS
经常使用到的关键字之一。经常在面试的时候,会被问到this
代表的具体含义,今天也本着学习的目的,仔细整理this
的知识点。
引子:this指向它的调用者
我们可以看一个例子
var obj = {
a: '我是obj',
foo:function(){
console.log(this.a)
}
}
var a = '我是window'
var foo = obj.foo
obj.foo() // 我是obj
foo() // 我是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); //1
obj.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); //Window
console.log(typeof this); //object
console.log(this === window); //true
函数(运行内)环境
函数内部直接出现this
在函数执行环境中使用this
时,如果函数没有明显的作为非window对象的属性,而只是定义了函数,不管这个函数是不是定义在另一个函数中,这个函数中的this
仍然表示window
// "use strict"
function A(){
//在A函数中定义一个B函数
function B(){
console.log(this); //Window
console.log(typeof this); //object
console.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->obj
whatsThis.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,添加方法objFun
var obj = {
name: 'jack',
objFun: function(){
console.log(this); // Object {name: "jack"}
console.log(typeof this); //object
console.log(this === window); //false
console.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 = objFun
obj.objFun();
// Object {name: "jack"}
// object
// false
// jack
这表明 函数是从obj
的objFun
成员调用 才是重点。
当我们简单的修改一下:
//定义一个对象obj,并为它添加属性name,添加方法objFun
var obj = {
name: 'jack',
objFun: function(){
console.log(this); // window
console.log(typeof this); //object
console.log(this === window); //true
console.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()) // lucy
const 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) // jack
window.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() //打出来的是window
obj.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() //没有使用箭头函数打出的是jack
obj.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) // Tony
var cat = new Cat()
console.log(cat.name) // I am mimi
虽然构造函数返回的默认值是this
所指的那个对象,但它仍可以手动返回其他的对象(如果返回值不是一个对象,则返回this
对象)。
总结
还有一些比较少见的用途,这里就不枚举了。总的来说,this
这个关键词清楚了调用它的函数的执行环境再去理解this
本身就会轻松许多!
现在面试的时候让你谈一谈this
就不会1分钟解决战斗了吧!
【参考资料】