执行上下文
我们知道执行上下文分为两种:全局上下文和函数上下文(我的这篇文章对于执行上下文有讲解还对执行上下文和作用域迷糊吗?)。全局上下文只有一个,函数执行上下文是在函数调用的时候创建的。
每个执行上下文都有三个属性:
- 变量对象
- 作用域链
- this
this到底是什么呢
this是在运行时绑定的,并不是在编写时绑定的,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何的关系,只取决于函数的调用方式。
调用位置
要理解this的绑定过程,首先要理解调用位置。调用位置就是函数的调用的位置(不是声明的位置)。所以我们要先来分析调用栈(也就是执行上下文栈)。我们先来看一段代码。
function baz() { console.log("baz") bar() } function bar() { console.log("bar") foo() } function foo() { console.log("foo") } baz()
当代码执行到foo(),进入foo的函数体,此时当前的调用栈为:
ECStack = [ fooContext, // foo barContext, // bar bazContext, // baz globalContext, // 全局 ]
通过调用栈我们就可以很清晰的找到函数的调用位置。baz在全局调用,bar在baz里调用,foo在bar里调用。
那函数在执行的时候是如何决定this的绑定对象的呢?
绑定规则
通过绑定规则决定this的绑定对象。
默认绑定
最常用的调用类型:独立函数调用。
function foo(){ console.log(this.a) // 2 } var a = 2; foo()
函数调用的时候,使用了this的默认绑定,因此this指向全局对象。
那么我们怎么知道这里应用了默认绑定呢?可以通过分析调用位置来看看 foo() 是如何调用的。在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。
所以,在全局环境中调用一个函数,函数内部的this指向的是全局变量window。
隐式绑定
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; obj.foo(); // 2
这段代码我们看到foo的声明位置是在全局的,但是它被当做引用属性添加到了obj中。调用位置使用obj上下文引用函数。当foo被调用时候,它是被obj对象所包含的,落脚点指向obj对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。
通过一个对象调用其内部的一个方法,该方法的执行上下文中的this指向对象本身
我们看个特殊的例子
function foo() { console.log(this.a) } var obj = { a: 2, foo } var bar = obj.foo var a = "mick" bar() // mick
bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
我们再看另一种情况
function foo() { console.log(this.a) } function doFoo(fn) { fn() } var obj = { a: 2, foo } var a = "mick" doFoo(obj.foo) // mick
嵌套函数中的this 不会从外层函数中继承。this永远指向最后调用它的那个对象
显示绑定
可以使用call、apply或bind方法。如果对这三个方法的实现原理感兴趣可以看看这篇手写call、apply、bind
function foo() { console.log(this.a) } var obj = { a: 2 } foo.call(obj) // 2
通过call方法,可以在调用foo时候,强制把它的this绑定到obj上。
new绑定
这里我们先说一下new来调用函数会发生哪些事情
- 创建一个全新的对象
- 这个新对象会被执行[[原型]]链接
- 这个新对象会绑定到函数调用的this
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo(a){ this.a = a } var bar = new foo(2) console.log(bar.a)
使用new来调用foo时,我们会构造一个新对象并把它绑定到foo调用中的this上。
特例
function foo() { console.log(this.a) } var a = 2 var o = { a: 3, foo: foo } var p = { a: 4 } o.foo() // 3 ;(p.foo = o.foo)() // 2
赋值表达式p.foo = o.foo
的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo()。所以这里是默认绑定。
面试题
下面我们看个面试题吧
var name = 'window' var person1 = { name: 'person1', foo1: function () { console.log(this.name) }, foo2: () => console.log(this.name), foo3: function () { return function () { console.log(this.name) } }, foo4: function () { return () => { console.log(this.name) } } } var person2 = { name: 'person2' } person1.foo1() person1.foo1.call(person2) person1.foo2() person1.foo2.call(person2) person1.foo3()() person1.foo3.call(person2)() person1.foo3().call(person2) person1.foo4()() person1.foo4.call(person2)() person1.foo4().call(person2)
我们一个个来解析一下。
-
person.foo1()
这个属于隐式绑定,foo1的this绑定到了person,所以打印person1 -
person1.foo1.call(person2)
显示绑定,foo1的this通过call改变了this,指向了person,所以打印person2 -
person.foo2()
因为foo2是箭头函数,所以它的this指向是它上一层this的指向也就是window,window -
person1.foo2.call(person2)
和上面一条同样的道理,也是window -
person1.foo3()()
内部返回了一个函数,其实是一个函数的引用,此时this应该指向window,所以打印window -
person1.foo3.call(person2)()
通过call只是改变了foo3的this指向,和返回的函数没有什么关系,所以this还是指向window,打印window -
person1.foo3().call(person2)
通过call改变了foo3内部返回函数的this指向,所以打印person2 -
person1.foo4()()
返回的是一个箭头函数,箭头函数的this是它上一层函数内部this的指向,所以也就是foo4this的指向,由于foo4被person1包含并调用,所以this指向person1,打印person1 -
person1.foo4.call(person2)()
,此时foo4内部的this通过call改变成了person2,所以打印person -
person1.foo4().call(person2)
这个和person1.foo4()
是一样的道理,打印person1
简单的谈了谈this的绑定,欢迎留言你的问题,大家一起学习一起进步!!!