1. 执行上下文
提到 this,还得从执行上下文说起。在执行上下文中,包含了变量环境、词法环境、外部环境、this:
实际上,this 是和执行上下文绑定的,也就是说每个执行上下文都有一个this,下面就来看看执行上下文的相关概念。
(1)执行上下文概念
执行上下文是评估和执行 JavaScript 代码的环境的抽象概念,当 JavaSciprt 代码在运行时,其运行在执行上下文中。JavaScript 中有三种执行上下文类型:
- 全局执行上下文: 任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文。
- 函数执行上下文: 当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。
- eval函数执行上下文: 执行在eval函数中的代码会有属于它自己的执行上下文。
由于eval函数执行上下文用的不多,所以这里只介绍全局执行上下文和函数执行上下文:
- 在全局执行上下文中,this 是指向 window 对象的;
- 在函数执行上下文中,默认情况下调用一个函数,其执行上下文的 this 也是指向 window 的。
(2)执⾏上下⽂栈
我们知道,浏览器中的JavaScript解释器是单线程的,也就是说浏览器同⼀时间只能做⼀个事情。代码中只有⼀个全局执⾏上下⽂和⽆数个函数执⾏上下⽂,这些组成了执⾏上下⽂栈(Execution Stack)。 ⼀个函数的执⾏上下⽂,在函数执⾏完毕后,会被移出执⾏上下⽂栈。看下面的例子:
function c(){ console.log('ok'); } function a(){ function b(){ c(); } b(); } a(); 复制代码
这段代码的执⾏上下⽂栈是这样的:
2. 函数的 this 指向
this 是 JavaScript 的一个关键字,多数情况下 this 指向调用它的对象。
这句话有两个意思,首先 this 指向的应该是一个对象(函数执行上下文对象)。其次,这个对象指向的是调用它的对象,如果调用它的不是对象或对象不存在,则会指向全局对象(严格模式下为 undefined)。
其实,this 是在函数被调用时确定的,它的指向取决于函数调用的地方,而不是它被声明的地方(除箭头函数外)。当函数被调用时,会创建一个执行上下文,它包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息,this 就是这个记录的一个属性,它会在函数执行的过程中被用到。
this 在函数的指向绑定形式有四种:默认绑定、隐式绑定、显式绑定、new绑定。
(1)默认绑定(全局环境)
函数在浏览器全局环境中直接使用不带任何修饰的函数引用进行调用,非严格模式下 this
指向 window
;在 use strict
指明严格模式的情况下就是 undefined
(严格模式不允许 this 指向全局对象)。
注意: 在浏览器环境下,全局对象是window;在Node.js环境下,全局对象是global。
function fn1 () { console.log(this) } function fn2 () { 'use strict' console.log(this) } fn1() // window fn2() // undefined 复制代码
需要注意一种情况,来看代码:
var num = 1 var foo = { num: 10, fn: function() { console.log(this) console.log(this.num) } } var fn1 = foo.fn fn1() 复制代码
这里会输出 Window 和 1。这里 this
仍然指向 window
。虽然 fn
函数在 foo
对象中作为方法被引用,但是在赋值给 fn1
之后,fn1
的执行仍然是在 window
全局环境中。因此输出 window
和 1
,它们相当于:
console.log(window) console.log(window.num) 复制代码
(2)隐式绑定(上下文对象)
如果函数在某个上下文对象中调用,那么 this 绑定的是那个上下文对象。来看一个例子:
var a = 'hello' var obj = { a: 'world', fn: function() { console.log(this.a) } } obj.fn() 复制代码
在上述代码中,最后会输出"world"。这里fn方法是作为对象的属性调用的,此时fn方法执行时,this会指向obj对象。也就是说,此时this指向的是调用这个方法的对象。
那如果嵌套了多层对象,this的指向又是怎样的呢?下面来看一个例子:
const obj1 = { text: 1, fn: function() { return this.text } } const obj2 = { text: 2, fn: function() { return obj1.fn() } } const obj3 = { text: 3, fn: function() { var fn = obj1.fn return fn() } } console.log(obj1.fn()) console.log(obj2.fn()) console.log(obj3.fn()) 复制代码
对于这段代码,最终的三个输出结果:
- 第一个
console
输出1
,这时 this 指向了调用 fn 方法的对象 obj1,所以会输出obj1中的属性text
的值1
; - 第二个
console
输出1
,这里调用了obj2.fn()
,最终还是调用o1.fn()
,因此仍然会输出1
。 - 第二个
console
输出undefined
,在进行var fn = o1.fn
赋值之后,是直接调用的,因此这里的this
指向window
,答案是undefined
。
根据上面例子可以得出结论:如果嵌套了多个对象,那么指向最后一个调用这个方法的对象。
那如何让 console.log(obj2.fn())
输出 2
呢?可以这样:
const obj1 = { text: 1, fn: function() { return this.text } } const obj2 = { text: 2, fn: o1.fn } console.log(obj2.fn()) 复制代码
还是上面的结论:this
指向最后调用它的对象,在 fn
执行时,挂到 obj2
对象上即可,就相当于提前进行了赋值操作。
(3)显示绑定(apply、call、bind)
显式绑定是指需要引用一个对象时进行强制绑定调用,显式绑定可以使用apply、call、bind
方法来绑定this
值,使其指向我们指定的对象。
call、apply 和 bind三个方法都可以改变函数 this
指向,但是 call 和 apply 是直接进行函数调用;bind
不会执行函数,而是返回一个新的函数,这个新的函数已经自动绑定了新的 this
指向,需要我们手动调用。call 和 apply 的区别: call 方法接受的是参数列表,而 apply 方法接受的是一个参数数组。
这三个方法的使用形式如下:
const target = {} fn.call(target, 'arg1', 'arg2') fn.apply(target, ['arg1', 'arg2']) fn.bind(target, 'arg1', 'arg2')() 复制代码
需要注意,如果把 null 或 undefined 作为 this 的绑定对象传入 call、apply、bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:
var a = 'hello' function fn() { console.log(this.a) } fn.call(null) 复制代码
这里会输出 hello,因为将 null 作为 this 传给了 call 方法,这时 this 会使用默认的绑定规则,this指向了全局对象 window,所以输出 window 中 a 的值 hello。
再来看一个例子:
const foo = { name: 'hello', logName: function() { console.log(this.name) } } const bar = { name: 'world' } console.log(foo.logName.call(bar)) 复制代码
这里将会输出:world。
那如果对一个函数进行多次 bind
,那么上下文会是什么呢?
let a = {} let fn = function () { console.log(this) } fn.bind().bind(a)() 复制代码
这里会输出 a
吗?可以把上述代码转换成另一种形式:
// fn.bind().bind(a) 等于 let fn2 = function fn1() { return function() { return fn.apply() }.apply(a) } fn2() 复制代码
可以发现,不管给函数 bind
几次,fn
中的 this
永远由第一次 bind
决定,所以结果永远是 window
。
let a = { name: 'CUGGZ' } function fn() { console.log(this.name) } fn.bind(a)() // CUGGZ 复制代码