前言
当一个函数调用时,会创建一个执行上下文,这个上下文包含函数调用的一些信息(调用栈,传入参数,调用方式),this就指向这个执行上下文
this不是静态的,也不是在编写的时候绑定的,而是在运行时绑定的。它的绑定和函数声明的位置没有
关系,只取决于函数的调用的方式
在开始前,我们先陈列本篇设计的内容
- 默认绑定
- 隐式绑定
- 隐式绑定丢失
- 显示绑定
- 显示绑定丢失
- new绑定
- 箭头函数绑定
- 综合题
- 总结
this指向哪里
在js中,我们必须了解this的绑定规则,this的绑定规则如下
- 默认绑定
- 隐式绑定
- 显示绑定
- new绑定
- ES6新增箭头函数绑定
接下来问我们一一讲述
默认绑定
默认绑定通常指的是函数的独立调用,不涉及其他绑定,非严格模式下,this指向window,严格模式下,this 指向 undefinded
1.1 非严格模式
`var foo = 123;
function print(){
this.foo = 234;
console.log(this); // window
console.log(foo); // 234
}
print();`
print()为默认绑定,this指向window,所以打印window和234。
这个foo值可以说道两句: 如果学习过预编译的知识,在预编译过程中,foo和print函数会存放在全局GO中(即window对象上),所以上述代码就类似下面这样
`
window.foo = 123
function print() {
this.foo = 234;
console.log(this);
console.log(window.foo);
}
window.print()`
1.2 严格模式下
加入 "use strict" 开启严格模式
"use strict";
var foo = 123;
function print(){
console.log('print this is ', this);
console.log(window.foo)
console.log(this.foo);
}
console.log('global this is ', this);
print();
1.3 let/const
let a = 1;
const b = 2;
var c = 3;
function print() {
console.log(this.a); undefined
console.log(this.b);undefined
console.log(this.c); 3
}
print();
console.log(this.a); undefined
let/const定义的变量存在暂时性死区,而且不会挂载到window对象上,因此print中是无法获取到a和b的
1.4 对象内执行
a = 1;
function foo() {
console.log(this.a);
}
const obj = {
a: 10,
bar() {
foo(); // 1
}
}
obj.bar();
foo虽然在obj的bar函数中,但foo函数仍然是独立运行的,foo中的this依旧指向window对象。
1.5 函数内执行
var a = 1
function outer () {
var a = 2
function inner () {
console.log(this.a) // 1
}
inner()
}
outer()
这个题与题目1.4类似,但要注意,不要把它看成闭包问题
自执行函数
a = 1;
(function(){
console.log(this); Window{...}
console.log(this.a) 1
}())
function bar() {
b = 2;
(function(){
console.log(this); Window{...}
console.log(this.b) 2
}())
}
bar();
注意 var
*默认情况下,自执行函数的this指向window
b是imply global,会挂载到window上
隐式绑定
函数的调用是在某个对象上触发的,即调用位置存在上下文对象,通俗点说就是**XXX.func()**这种调用模式。
此时func的this指向XXX,但如果存在链式调用,例如XXX.YYY.ZZZ.func,记住一个原则:this永远指向最后调用它的那个对象。
2.1:隐式绑定
var a = 1;
function foo() {
console.log(this.a);
}
// 对象简写,等同于 {a:2, foo: foo}
var obj = {a: 2, foo}
foo(); 1
obj.foo(); 2
foo(): 默认绑定,打印1obj.foo(): 隐式绑定,打印2
obj是通过var定义的,obj会挂载到window之上的,obj.foo()就相当于window.obj.foo(),这也印证了this永远指向最后调用它的那个对象规则。
2.2:对象链式调用
var obj1 = {
a: 1,
obj2: {
a: 2,
foo(){
console.log(this.a)
}
}
}
obj1.obj2.foo() // 2
3 隐式绑定的丢失
隐式绑定可是个调皮的东西,一不小心它就会发生绑定的丢失。一般会有两种常见的丢失:
- 使用另一个变量作为函数别名,之后使用别名执行函数
- 将函数作为参数传递时会被隐式赋值
隐式绑定丢失之后,this的指向会启用默认绑定。
具体来看题目:
3.1:取函数别名
a = 1
var obj = {
a: 2,
foo() {
console.log(this.a)
}
}
var foo = obj.foo;
obj.foo(); 2
foo(); 1
JavaScript对于引用类型,其地址指针存放在栈内存中,真正的本体是存放在堆内存中的。
上面将obj.foo赋值给foo,就是将foo也指向了obj.foo所指向的堆内存,此后再执行foo,相当于直接执行的堆内存的函数,与obj无关,foo为默认绑定。笼统的记,只要fn前面什么都没有,肯定不是隐式绑定。
不要把这里理解成window.foo执行,如果foo为let/const定义,foo不会挂载到window上,但不会影响最后的打印结果
3.2取函数别名
如果取函数别名没有发生在全局,而是发生在对象之中,又会是怎样的结果呢?
var obj = {
a: 1,
foo() {
console.log(this.a)
}
};
var a = 2;
var foo = obj.foo;
var obj2 = { a: 3, foo: obj.foo }
obj.foo(); 1
foo(); 2
obj2.foo(); 3
obj2.foo指向了obj.foo的堆内存,此后执行与obj无关(除非使用call/apply改变this指向)
3.3:函数作为参数传递
function foo() {
console.log(this.a)
}
function doFoo(fn) {
console.log(this)
fn()
}
var obj = { a: 1, foo }
var a = 2
doFoo(obj.foo)
用函数预编译的知识来解答这个问题:函数预编译四部曲前两步分别是:
- 找形参和变量声明,值赋予
undefined - 将形参与实参相统一,也就是将实参的值赋予形参。
obj.foo作为实参,在预编译时将其值赋值给形参fn,是将obj.foo指向的地址赋给了fn,此后fn执行不会与obj产生任何关系。fn为默认绑定
3.4:函数作为参数传递
将上面的题略作修改,doFoo不在window上执行,改为在obj2中执行
function foo() {
console.log(this.a)
}
function doFoo(fn) {
console.log(this)
fn()
}
var obj = { a: 1, foo }
var a = 2
var obj2 = { a: 3, doFoo }
obj2.doFoo(obj.foo)
console.log(this):obj2.doFoo符合xxx.fn格式,doFoo的为隐式绑定,this为obj2,打印{a: 3, doFoo: ƒ}fn(): 没有于obj2产生联系,默认绑定,打印2
答案
{a: 3, doFoo: ƒ}
2
3.5:回调函数
var name='zcxiaobao';
function introduce(){
console.log('Hello,My name is ', this.name);
}
const Tom = {
name: 'TOM',
introduce: function(){
setTimeout(function(){
console.log(this)
console.log('Hello, My name is ',this.name);
})
}
}
const Mary = {
name: 'Mary',
introduce
}
const Lisa = {
name: 'Lisa',
introduce
}
Tom.introduce();
setTimeout(Mary.introduce, 100);
setTimeout(function(){
Lisa.introduce();
},200);
setTimeout是异步调用的,只有当满足条件并且同步代码执行完毕后,才会执行它的回调函数。
-
Tom.introduce()执行:console位于setTimeout的回调函数中,回调函数的this指向window -
Mary.introduce直接作为setTimeout的函数参数(类似题目题目3.3),会发生隐式绑定丢失,this为默认绑定 -
Lisa.introduce执行虽然位于setTimeout的回调函数中,但保持xxx.fn模式,this为隐式绑定。Window {…} Hello, My name is zcxiaobao Hello,My name is zcxiaobao Hello,My name is Lisa
所以如果我们想在setTimeout或setInterval中使用外界的this,需要提前存储一下,避免this的丢失。
const Tom = {
name: 'TOM',
introduce: function(){
_self = this
setTimeout(function(){
console.log('Hello, My name is ',_self.name);
})
}
}
Tom.introduce()
3.6:隐式绑定丢失综合题
name = 'javascript' ;
let obj = {
name: 'obj',
A (){
this.name += 'this';
console.log(this.name)
},
B(f){
this.name += 'this';
f();
},
C(){
setTimeout(function(){
console.log(this.name);
},1000);
}
}
let a = obj.A;
a(); javascriptthis
obj.B(function(){
console.log(this.name); javascriptthis
});
obj.C(); javascriptthis
console.log(name); javascriptthis
4.显式绑定
显式绑定比较好理解,就是通过call()、apply()、bind()等方法,强行改变this指向。
上面的方法虽然都可以改变this指向,但使用起来略有差别:
call()和apply()函数会立即执行bind()函数会返回新函数,不会立即执行函数call()和apply()的区别在于call接受若干个参数,apply接受数组。
4.1:比较三种调用方式
function foo () {
console.log(this.a)
}
var obj = { a: 1 }
var a = 2
foo()
foo.call(obj)
foo.apply(obj)
foo.bind(obj)
foo(): 默认绑定。foo.call(obj): 显示绑定,foo的this指向objfoo.apply(obj): 显式绑定foo.bind(obj): 显式绑定,但不会立即执行函数,没有返回值 2 1 1
4.2 隐式绑定丢失
发生隐式绑定的丢失,如下代码:我们可不可以通过显式绑定来修正这个问题。
function foo() {
console.log(this.a)
}
function doFoo(fn) {
console.log(this)
fn()
}
var obj = { a: 1, foo }
var a = 2
doFoo(obj.foo)
复制代码
- 首先先修正
doFoo()函数的this指向。
doFoo.call(obj, obj.foo)
复制代码
- 然后修正
fn的this。
function foo() {
console.log(this.a)
}
function doFoo(fn) {
console.log(this)
fn.call(this)
}
var obj = { a: 1, foo }
var a = 2
doFoo(obj.foo)
4.3:回调函数与call
var obj1 = {
a: 1
}
var obj2 = {
a: 2,
bar: function () {
console.log(this.a)
},
foo: function () {
setTimeout(function () {
console.log(this)
console.log(this.a)
}.call(obj1), 0)
}
}
var a = 3
obj2.bar()
obj2.foo()
答案
2
{a: 1}
1
4.4:注意call位置
function foo () {
console.log(this.a)
}
var obj = { a: 1 }
var a = 2
foo()
foo.call(obj)
foo().call(obj)
foo(): 默认绑定foo.call(obj): 显式绑定foo().call(obj): 对foo()执行的返回值执行call,foo返回值为undefined,执行call()会报错
答案
2
1
2
Uncaught TypeError: Cannot read property 'call' of undefined
4.5:注意call位置(2)
function foo () {
console.log(this.a)
return function() {
console.log(this.a)
}
}
var obj = { a: 1 }
var a = 2
foo()
foo.call(obj)
foo().call(obj)
foo(): 默认绑定foo.call(obj): 显式绑定foo().call(obj):foo()执行,打印2,返回匿名函数通过call将this指向obj,打印1。
这里千万注意:最后一个foo().call(obj)有两个函数执行,会打印2个值。
答案
2 1 2 1
4.6:bind
将上面的call全部换做bind函数,又会怎样那?
call是会立即执行函数,bind会返回一个新函数,但不会执行函数
function foo () {
console.log(this.a)
return function() {
console.log(this.a)
}
}
var obj = { a: 1 }
var a = 2
foo()
foo.bind(obj)
foo().bind(obj)
复制代码
首先我们要先确定,最后会输出几个值?bind不会执行函数,因此只有两个foo()会打印a。
foo(): 默认绑定,打印2foo.bind(obj): 返回新函数,不会执行函数,无输出foo().bind(obj): 第一层foo(),默认绑定,打印2,后bind将foo()返回的匿名函数this指向obj,不执行
答案
2
2
4.7:外层this与内层this
做到这里,不由产生了一些疑问:如果使用call、bind等修改了外层函数的this,那内层函数的this会受影响吗? (注意区别箭头函数)
function foo () {
console.log(this.a)
return function() {
console.log(this.a)
}
}
var obj = { a: 1 }
var a = 2
foo.call(obj)()
复制代码
foo.call(obj): 第一层函数foo通过call将this指向obj,打印1;第二层函数为匿名函数,默认绑定,打印2。
答案
1
2
4.8:对象中的call
把上面的代码移植到对象中,看看会发生怎样的变化?
var obj = {
a: 'obj',
foo: function () {
console.log('foo:', this.a)
return function () {
console.log('inner:', this.a)
}
}
}
var a = 'window'
var obj2 = { a: 'obj2' }
obj.foo()()
obj.foo.call(obj2)()
obj.foo().call(obj2)
复制代码
看着这么多括号,是不是感觉有几分头大。没事,咱们来一层一层分析:
obj.foo()(): 第一层obj.foo()执行为隐式绑定,打印出foo:obj;第二层匿名函数为默认绑定,打印inner:windowobj.foo.call(obj2)(): 类似题目4.7,第一层obj.foo.call(obj2)使用call将obj.foo的this指向obj2,打印foo: obj2;第二层匿名函数默认绑定,打印inner:windowobj.foo().call(obj2): 类似题目4.5,第一层隐式绑定,打印:foo: obj,第二层匿名函数使用call将this指向obj2,打印inner: obj2
4.9:带参数的call
显式绑定一开始讲的时候,就谈过call/apply存在传参差异,那咱们就来传一下参数,看看传完参数的this会是怎样的美妙。
var obj = {
a: 1,
foo: function (b) {
b = b || this.a
return function (c) {
console.log(this.a + b + c)
}
}
}
var a = 2
var obj2 = { a: 3 }
obj.foo(a).call(obj2, 1)
obj.foo.call(obj2)(1)
复制代码
要注意call执行的位置:
-
obj.foo(a).call(obj2, 1):obj.foo(a): foo的AO中b值为传入的a(形参与实参相统一),值为2,返回匿名函数fn- 匿名函数
fn.call(obj2, 1): fn的this指向为obj2,c值为1 this.a + b + c = obj2.a + FooAO.b + c = 3 + 2 + 1 = 6
-
obj.foo.call(obj2)(1):obj.foo.call(obj2): obj.foo的this指向obj2,未传入参数,b = this.a = obj2.a = 3;返回匿名函数fn- 匿名函数
fn(1): c = 1,默认绑定,this指向window this.a + b + c = window.a + obj2.a + c = 2 + 3 + 1 = 6
答案
6
6
5.显式绑定扩展
上面提了很多call/apply可以改变this指向,但都没有太多实用性。下面来一起学几个常用的call与apply使用。
题目5.1:apply求数组最值
JavaScript中没有给数组提供类似max和min函数,只提供了Math.max/min,用于求多个数的最值,所以可以借助apply方法,直接传递数组给Math.max/min
const arr = [1,10,11,33,4,52,17]
Math.max.apply(Math, arr)
Math.min.apply(Math, arr)
复制代码
题目5.2:类数组转为数组
ES6未发布之前,没有Array.from方法可以将类数组转为数组,采用Array.prototype.slice.call(arguments)或[].slice.call(arguments)将类数组转化为数组。
题目5.3:数组高阶函数
日常编码中,我们会经常用到forEach、map等,但这些数组高阶方法,它们还有第二个参数thisArg,每一个回调函数都是显式绑定在thisArg上的。
例如下面这个例子
const obj = {a: 10}
const arr = [1, 2, 3, 4]
arr.forEach(function (val, key){
console.log(`${key}: ${val} --- ${this.a}`)
}, obj)
复制代码
答案
0: 1 --- 10
1: 2 --- 10
2: 3 --- 10
3: 4 --- 10
6.new绑定
function User(name, age) {
this.name = name;
this.age = age;
}
var name = 'Tom';
var age = 18;
var zc = new User('zc', 24);
console.log(zc.name)
复制代码
答案
zc
复制代码
6.2:属性加方法
function User (name, age) {
this.name = name;
this.age = age;
this.introduce = function () {
console.log(this.name)
}
this.howOld = function () {
return function () {
console.log(this.age)
}
}
}
var name = 'Tom';
var age = 18;
var zc = new User('zc', 24)
zc.introduce()
zc.howOld()()
复制代码
这个题很难不让人想到如下代码,都是函数嵌套,具体解法是类似的,可以对比来看一下啊。
const User = {
name: 'zc';
age: 18;
introduce = function () {
console.log(this.name)
}
howOld = function () {
return function () {
console.log(this.age)
}
}
}
var name = 'Tom';
var age = 18;
User.introduce()
User.howOld()()
复制代码
zc.introduce(): zc是new创建的实例,this指向zc,打印zczc.howOld()(): zc.howOld()返回一个匿名函数,匿名函数为默认绑定,因此打印18(阿包永远18)
答案
zc
18
7.箭头函数
箭头函数没有自己的this,它的this指向外层作用域的this,且指向函数定义时的this而非执行时。
this指向外层作用域的this: 箭头函数没有this绑定,但它可以通过作用域链查到外层作用域的this指向函数定义时的this而非执行时:JavaScript是静态作用域,就是函数定义之后,作用域就定死了,跟它执行时的地方无关。更详细的介绍见JavaScript之静态作用域与动态作用域。
7.1:对象方法使用箭头函数
name = 'tom'
const obj = {
name: 'zc',
intro: () => {
console.log('My name is ' + this.name)
}
}
obj.intro()
复制代码
上文说到,箭头函数的this通过作用域链查到,intro函数的上层作用域为window。
答案
My name is tom
复制代码
7.2:箭头函数与普通函数比较
name = 'tom'
const obj = {
name: 'zc',
intro:function () {
return () => {
console.log('My name is ' + this.name)
}
},
intro2:function () {
return function() {
console.log('My name is ' + this.name)
}
}
}
obj.intro2()()
obj.intro()()
复制代码
obj.intro2()(): 不做赘述,打印My name is tomobj.intro()():obj.intro()返回箭头函数,箭头函数的this取决于它的外层作用域,因此箭头函数的this指向obj,打印My name is zc
7.3:箭头函数与普通函数的嵌套
name = 'window'
const obj1 = {
name: 'obj1',
intro:function () {
console.log(this.name)
return () => {
console.log(this.name)
}
}
}
const obj2 = {
name: 'obj2',
intro: ()=> {
console.log(this.name)
return function() {
console.log(this.name)
}
}
}
const obj3 = {
name: 'obj3',
intro: ()=> {
console.log(this.name)
return () => {
console.log(this.name)
}
}
}
obj1.intro()()
obj2.intro()()
obj3.intro()()
复制代码
obj1.intro()(): 类似题目7.2,打印obj1,obj1obj2.intro()():obj2.intro()为箭头函数,this为外层作用域this,指向window。返回匿名函数为默认绑定。打印window,windowobj3.intro()():obj3.intro()与obj2.intro()相同,返回值为箭头函数,外层作用域intro的this指向window,打印window,window
答案
obj1
obj1
window
window
window
window
复制代码
7.4:new碰上箭头函数
function User(name, age) {
this.name = name;
this.age = age;
this.intro = function(){
console.log('My name is ' + this.name)
},
this.howOld = () => {
console.log('My age is ' + this.age)
}
}
var name = 'Tom', age = 18;
var zc = new User('zc', 24);
zc.intro();
zc.howOld();
复制代码
zc是new User实例,因此构造函数User的this指向zczc.intro(): 打印My name is zczc.howOld():howOld为箭头函数,箭头函数this由外层作用域决定,且指向函数定义时的this,外层作用域为User,this指向zc,打印My age is 24
7.5:call碰上箭头函数
箭头函数由于没有this,不能通过call\apply\bind来修改this指向,但可以通过修改外层作用域的this来达成间接修改
var name = 'window'
var obj1 = {
name: 'obj1',
intro: function () {
console.log(this.name)
return () => {
console.log(this.name)
}
},
intro2: () => {
console.log(this.name)
return function () {
console.log(this.name)
}
}
}
var obj2 = {
name: 'obj2'
}
obj1.intro.call(obj2)()
obj1.intro().call(obj2)
obj1.intro2.call(obj2)()
obj1.intro2().call(obj2)
复制代码
obj1.intro.call(obj2)(): 第一层函数为普通函数,通过call修改this为obj2,打印obj2。第二层函数为箭头函数,它的this与外层this相同,同样打印obj2。obj1.intro().call(obj2): 第一层函数打印obj1,第二次函数为箭头函数,call无效,它的this与外层this相同,打印obj1obj1.intro2.call(obj2)(): 第一层为箭头函数,call无效,外层作用域为window,打印window;第二次为普通匿名函数,默认绑定,打印windowobj1.intro2().call(obj2): 与上同,打印window;第二层为匿名函数,call修改this为obj2,打印obj2
答案
obj2
obj2
obj1
obj1
window
window
window
obj2
复制代码
8.箭头函数扩展
总结
- 箭头函数没有
this,它的this是通过作用域链查到外层作用域的this,且指向函数定义时的this而非执行时。 - 不可以用作构造函数,不能使用
new命令,否则会报错 - 箭头函数没有
arguments对象,如果要用,使用rest参数代替 - 不可以使用
yield命令,因此箭头函数不能用作Generator函数。 - 不能用
call/apply/bind修改this指向,但可以通过修改外层作用域的this来间接修改。 - 箭头函数没有
prototype属性。
避免使用场景
- 箭头函数定义对象方法
const zc = {
name: 'zc',
intro: () => {
// this -> window
console.log(this.name)
}
}
zc.intro() // undefined
复制代码
- 箭头函数不能作为构造函数
const User = (name, age) => {
this.name = name;
this.age = age;
}
// Uncaught TypeError: User is not a constructor
zc = new User('zc', 24);
复制代码
- 事件的回调函数
DOM中事件的回调函数中this已经封装指向了调用元素,如果使用构造函数,其this会指向window对象
document.getElementById('btn')
.addEventListener('click', ()=> {
console.log(this === window); // true
})
复制代码
9.综合题
学完上面的知识,是不是感觉自己已经趋于化境了,现在就一起来华山之巅一决高下吧。
9.1: 对象综合体
var name = 'window'
var user1 = {
name: 'user1',
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 user2 = { name: 'user2' }
user1.foo1()
user1.foo1.call(user2)
user1.foo2()
user1.foo2.call(user2)
user1.foo3()()
user1.foo3.call(user2)()
user1.foo3().call(user2)
user1.foo4()()
user1.foo4.call(user2)()
user1.foo4().call(user2)
复制代码
这个题目并不难,就是把上面很多题做了个整合,如果上面都学会了,此题问题不大。
user1.foo1()、user1.foo1.call(user2): 隐式绑定与显式绑定user1.foo2()、user1.foo2.call(user2): 箭头函数与calluser1.foo3()()、user1.foo3.call(user2)()、user1.foo3().call(user2): 见题目4.8user1.foo4()()、user1.foo4.call(user2)()、user1.foo4().call(user2): 见题目7.5
答案:
var name = 'window'
var user1 = {
name: 'user1',
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 user2 = { name: 'user2' }
user1.foo1() // user1
user1.foo1.call(user2) // user2
user1.foo2() // window
user1.foo2.call(user2) // window
user1.foo3()() // window
user1.foo3.call(user2)() // window
user1.foo3().call(user2) // user2
user1.foo4()() // user1
user1.foo4.call(user2)() // user2
user1.foo4().call(user2) // user1
复制代码
9.2:隐式绑定丢失
var x = 10;
var foo = {
x : 20,
bar : function(){
var x = 30;
console.log(this.x)
}
};
foo.bar();
(foo.bar)();
(foo.bar = foo.bar)();
(foo.bar, foo.bar)();
复制代码
突然出现了一个代码很少的题目,还乍有些不习惯。
foo.bar(): 隐式绑定,打印20(foo.bar)(): 上面提到过运算符优先级的知识,成员访问与函数调用优先级相同,默认从左到右,因此括号可有可无,隐式绑定,打印20(foo.bar = foo.bar)():隐式绑定丢失,给foo.bar起别名,虽然名字没变,但是foo.bar上已经跟foo无关了,默认绑定,打印10(foo.bar, foo.bar)(): 隐式绑定丢失,起函数别名,将逗号表达式的值(第二个foo.bar)赋值给新变量,之后执行新变量所指向的函数,默认绑定,打印10。
上面那说法有可能有几分难理解,隐式绑定有个定性条件,就是要满足
XXX.fn()格式,如果破坏了这种格式,一般隐式绑定都会丢失。
9.3:arguments(推荐看)
var length = 10;
function fn() {
console.log(this.length);
}
var obj = {
length: 5,
method: function(fn) {
fn();
arguments[0]();
}
};
obj.method(fn, 1);
复制代码
这个题要注意一下,有坑。
-
fn(): 默认绑定,打印10 -
arguments[0](): 这种执行方式看起来就怪怪的,咱们把它展开来看看:arguments是一个类数组,arguments展开,应该是下面这样:
arguments: { 0: fn, 1: 1, length: 2 } 复制代码arguments[0]: 这是访问对象的属性0?0不好理解,咱们把它稍微一换,方便一下理解:
arguments: { fn: fn, 1: 1, length: 2 } 复制代码- 到这里大家应该就懂了,隐式绑定,
fn函数this指向arguments,打印2
9.4:压轴题(推荐看)
var number = 5;
var obj = {
number: 3,
fn: (function () {
var number;
this.number *= 2;
number = number * 2;
number = 3;
return function () {
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
}
})()
}
var myFun = obj.fn;
myFun.call(null);
obj.fn();
console.log(window.number);
复制代码
fn.call(null)或者fn.call(undefined)都相当于fn()
-
obj.fn为立即执行函数: 默认绑定,this指向window我们来一句一句的分析:
var number: 立即执行函数的AO中添加number属性,值为undefinedthis.number *= 2:window.number = 10number = number * 2: 立即执行函数AO中number值为undefined,赋值后为NaNnumber = 3:AO中number值由NaN修改为3- 返回匿名函数,形成闭包
此时的obj可以类似的看成以下代码(注意存在闭包):
obj = { number: 3, fn: function () { var num = this.number; this.number *= 2; console.log(num); number *= 3; console.log(number); } } 复制代码 -
myFun.call(null): 相当于myFun(),隐式绑定丢失,myFun的this指向window。依旧一句一句的分析:
var num = this.number:this指向window,num = window.num = 10this.number *= 2:window.number = 20console.log(num): 打印10number *= 3: 当前AO中没有number属性,沿作用域链可在立即执行函数的AO中查到number属性,修改其值为9console.log(number): 打印立即执行函数AO中的number,打印9
-
obj.fn(): 隐式绑定,fn的this指向obj继续一步一步的分析:
var num = this.number:this->obj,num = obj.num = 3this.number *= 2:obj.number *= 2 = 6console.log(num): 打印num值,打印3number *= 3: 当前AO中不存在number,继续修改立即执行函数AO中的number,number *= 3 = 27console.log(number): 打印27
-
console.log(window.number): 打印20
这里解释一下,为什么
myFun.call(null)执行时,找不到number变量,是去找立即执行函数AO中的number,而不是找window.number: JavaScript采用的静态作用域,当定义函数后,作用域链就已经定死。(更详细的解释文章最开始的推荐中有)
答案
10
9
3
27
20
复制代码
总结
- 默认绑定: 非严格模式下
this指向全局对象,严格模式下this会绑定到undefined - 隐式绑定: 满足
XXX.fn()格式,fn的this指向XXX。如果存在链式调用,this永远指向最后调用它的那个对象 - 隐式绑定丢失:起函数别名,通过别名运行;函数作为参数会造成隐式绑定丢失。
- 显示绑定: 通过
call/apply/bind修改this指向 new绑定: 通过new来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的this。- 箭头函数绑定: 箭头函数没有
this,它的this是通过作用域链查到外层作用域的this,且指向函数定义时的this而非执行时