欢迎大家关注,接下来我会写一个关于
JavaScirpt
系列文章,希望我们一起进步。
前言
this
关键字是 JavaScript
中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。作为一名前端攻城狮对它再熟悉不过了,然而正是因为熟悉它所以很容易忽略它,以至于用它时踩了不少的坑,甚至在面试时还因为它挂了。所以学习和掌握 this
的用法和一些陷阱对于进阶成名一名合格前端攻城狮很有必要。
this 误解
正所谓先破而后立,我们首先解除一下长时间对 this
的误解,再开始 this
学习之旅。
一直以来我们可能以为 this
是指向函数自身或者函数的词法作用域,这在某种情况下是可行的,但是还是不够。this
在未执行时我们谁也不知道它的指向到底是谁,因为只有函数被调用时才会对 this
进行赋值,所以要知道 this
指向谁,首先知道它是在什么位置被调用的。
调用位置
调用位置是函数在代码中调用的位置(而不是声明的位置)。这句话好像看起来像是废话,不过在它面前踩过坑的人觉得这句话说的太精辟了(这就是我想说的)。
调用位置分为以下几种情况:
- 普通函数调用:在全局环境中调用函数
- 对象方法调用:通过对象的方法调用
- 构造器调用:使用
new
运算符实例化调用 - 显式调用:通过
call
、apply
、bind
调用,修正this
指向
普通函数调用
普通函数调用, this
指向全局对象。在浏览器 JS
引擎中this
指向 window
, Nodejs
环境 this
指向 global
function f1(){
return this;
}
// 在浏览器中,全局对象是 window
f1() === window // true
//在Node中,全局对象是 global
f1() === global // true
// 示例代码
var name = 'globalName'
var getName = function () {
return this.name
}
getName() // globalName
// or
var obj = function () {
name: 'John',
getName: function () {
return this.name
}
}
var getName = obj.getName
getName() // globalName
值得注意的是在最后两行代码,对象方法赋值给了 getName
变量,调用 getName()
相当于 调用window.getName()
,此时 this
是指向 window
陷阱
- 在严格模式下,
this
指向undefined
- 在全局环境中,使用
var
声明的变量会挂载在window
上,但let
、const
声明的变量,不会挂载在window
上
function f1(){
'use strict'
return this;
}
f1() // 严格模式下,this 指向 undefined
var a = 111
window.a // 111
let b = 222
const c = 333
window.b // undefined let、const 声明变量没有挂载在 window 上
window.c // undefined
对象方法调用
当函数作为对象的方法被调用时, this 指向该对象:
var obj = {
name: '张三',
getName: function () {
return this.name
}
}
obj.getName() // 张三
陷阱
使用对象方法调用时,this
有可能会丢失,看下面这段代码
var name = 'globalName'
var obj = {
name: '张三',
getName: function () {
function fn () {
return this.name
}
return fn() // globalName
}
}
obj.getName() // globalName
上面代码输出 globalName
而不是 张三
·,因为在 getName
函数内部调用 fn
, 此时 fn
函数执行上下文this
不是指向调用的对象 obj
,而是指向 window
构造器调用
除了宿主提供的一些内置函数,大部分 JavaScript
函数可以当作构造器使用。构造器表面和普通函数一模一样,不同的地方在于被调用的方式。
使用 new
运算符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的 this
就指向这个对象
var MyName = function () {
this.name = 'jeffery'
}
var obj = new MyName()
console.log(obj.name) // jeffery
使用 new
运算符创建 MyName
构造器,此时this
指向 obj
陷阱
使用 new
调用构造器时,还要注意一个问题,如果构造器显式返回一个 object
类型的对象,那么此次运行结果最终是返回这个对象,而不是我们之前期待的 this
:
var MyName = function () {
this.name = 'jeffery'
return { // 显示返回一个对象
name: 'this is myName'
}
}
var obj = new MyName()
// 输出 this is myName,而不是上面的 jeffery
console.log(obj.name) // this is myName
如果构造器不显式返回任何数据,或者返回一个非对象类型的数据,就不会存在上面这个问题
var MyName = function () {
this.name = 'jeffery'
return 'this is myName'
}
var obj = new MyName()
console.log(obj.name) // jeffery
call、apply、bind 显式调用修正 this 指向
call、apply 修正 this 指向
call
和 apply
调用函数和其他函数调用相比,它会改变传入函数的 this
, 指向第一个传入的参数。call
和 apply
两者实现功能相同, 不同的地方在于接收参数形式不一样,前者接收的是参数个数,后者接收的是一个数组
var obj1 = {
name: 'obj1 name',
getName: function () {
return this.name
}
}
var obj2 = {
name: 'obj2 name'
}
// 对象方法调用,this 指向 obj1
obj1.getName() // obj1 name
// 使用 call 显示调用,改变了原来 this 指向,指向了 obj2
obj1.getName.call(obj2) // obj2 name
陷阱
call
、apply
第一个参数除了可以是对象引用类型,也可以是基本类型:
null
或undefined
:this
指向window
;不过,在严格模式下,this
还是指向undefined
。number
、string
、boolean
:this
会指向其内置构造函数Number
、String
、Boolean
var name = 'globalName'
var obj = {
name: 'obj name',
getName: function () {
// 'use strict' // 严格模式下,this 指向 undefined, null 会报错
return this.name
},
getThis: function () {
return this
}
}
obj.getName.call(null) // globalName
obj.getName.apply(undefined) // globalName
// number boolean string
obj.getThis.call(111) // Number {111}
obj.getThis.call(true) // Boolean {true}
obj.getThis.call('str') // String {"str"}
bind 修正 this 指向
bind
和 call
、apply
不同的地方在于改变了this
指向同时会返回一个新的函数
function f(){
return this.a;
}
var g = f.bind({a:"azerty"});
console.log(g()); // azerty
var h = g.bind({a:'yoo'}); // bind只生效一次!
console.log(h()); // azerty
var o = {a:37, f:f, g:g, h:h};
console.log(o.f(), o.g(), o.h()); // 37, azerty, azerty
bind
绑定改变 this
只能生效一次,如果链式发生多次绑定以第一次为准
两道经典面试题
俗话说:“实践是验证真理的唯一标准”。很多时候我们以为学会了也只是自己以为学会了,是骡子还是马牵出来溜溜就知道了。所以,检验自己的学习成果莫过于实践。下面附上两道面试题让大家动脑实践一下
求解答为什么x.x调用结果会是undefined
function fn(xx){
this.x = xx;
return this;
}
var x = fn(5);
var y = fn(6);
console.log(x.x);
console.log(y.x);
下面的代码输出什么
let length = 10;
function fn() {
console.log(this.length);
}
var obj = {
length: 5,
method: function(fn) {
fn();
arguments[0]();
}
}
obj.method(fn, 1);
不知道答案的小伙伴可以戳这里:前端面试题(八)关于this指向的问题
引用链接
- MDN-this用法
- 全面解析 Javascript - this
- JavaScript 设计模式和开发实践
推荐阅读
- 深入了解JavaScript执行过程(JS系列之一)
- 深入学习作用域和闭包—全面(JS系列之二)