《2w字大章 38道面试题》彻底理清JS中this指向问题(一)

简介: 《2w字大章 38道面试题》彻底理清JS中this指向问题(一)

前言


当一个函数调用时,会创建一个执行上下文,这个上下文包括函数调用的一些信息(调用栈,传入参数,调用方式),this就指向这个执行上下文。


this不是静态的,也并不是在编写的时候绑定的,而是在运行时绑定的。它的绑定和函数声明的位置没有关系,只取决于函数调用的方式。


本篇文章有点长,涉及到很多道面试题,有难有简单,如果能耐心的通读一编,我相信以后this都不成问题。


学习this之前,建议先学习以下知识:



在文章的最开始,陈列一下本篇文章涉及的内容,保证让大家不虚此行。


  • 默认绑定
  • 隐式绑定
  • 隐式绑定丢失
  • 显式绑定
  • 显式绑定应用
  • new绑定
  • 箭头函数绑定
  • 综合题
  • 总结


this指向哪里


JavaScript中,要想完全理解this,首先要理解this的绑定规则,this的绑定规则一共有5种:


  1. 默认绑定
  2. 隐式绑定
  3. 显式(硬)绑定
  4. new绑定
  5. ES6新增箭头函数绑定


下面来一一介绍以下this的绑定规则。


1.默认绑定


默认绑定通常是指函数独立调用,不涉及其他绑定规则。非严格模式下,this指向window,严格模式下,this指向undefined


题目1.1:非严格模式

var foo = 123;
function print(){
  this.foo = 234;
    console.log(this); // window
  console.log(foo); // 234
}
print();  
复制代码


非严格模式,print()为默认绑定,this指向window,所以打印window234

这个foo值可以说道两句: 如果学习过预编译的知识,在预编译过程中,fooprint函数会存放在全局GO中(即window对象上),所以上述代码就类似下面这样:


window.foo = 123
function print() {
    this.foo = 234;
    console.log(this); 
  console.log(window.foo);
}
window.print()
复制代码


题目1.2:严格模式


题目1.1稍作修改,看看严格模式下的执行结果。


"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();
复制代码


注意事项:开启严格模式后,函数内部this指向undefined,但全局对象window不会受影响


答案

global this is Window{...}
print this is undefined
123
Uncaught TypeError: Cannot read property 'foo' of undefined
复制代码


题目1.3:let/const


let a = 1;
const b = 2;
var c = 3;
function print() {
    console.log(this.a);
    console.log(this.b);
    console.log(this.c);
}
print();
console.log(this.a);
复制代码


let/const定义的变量存在暂时性死区,而且不会挂载到window对象上,因此print中是无法获取到a和b的。


答案


undefined
undefined
3
undefined
复制代码


题目1.4:对象内执行


a = 1;
function foo() {
    console.log(this.a); 
}
const obj = {
    a: 10,
    bar() {
        foo(); // 1
    }
}
obj.bar(); 
复制代码


foo虽然在objbar函数中,但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类似,但要注意,不要把它看成闭包问题


题目1.6:自执行函数


a = 1;
(function(){
    console.log(this);
    console.log(this.a)
}())
function bar() {
    b = 2;
    (function(){
        console.log(this);
        console.log(this.b)
    }())
}
bar();
复制代码


默认情况下,自执行函数的this指向window


自执行函数只要执行到就会运行,并且只会运行一次,this指向window


答案


Window{...}
1
Window{...}
2 // b是imply global,会挂载到window上
复制代码


2.隐式绑定


函数的调用是在某个对象上触发的,即调用位置存在上下文对象,通俗点说就是**XXX.func()**这种调用模式。


此时functhis指向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();
obj.foo();
复制代码


  • foo(): 默认绑定,打印1
  • obj.foo(): 隐式绑定,打印2


答案


1
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();
foo();
复制代码


JavaScript对于引用类型,其地址指针存放在栈内存中,真正的本体是存放在堆内存中的。


上面将obj.foo赋值给foo,就是将foo也指向了obj.foo所指向的堆内存,此后再执行foo,相当于直接执行的堆内存的函数,与obj无关,foo为默认绑定。笼统的记,只要fn前面什么都没有,肯定不是隐式绑定


答案


2 
1
复制代码


不要把这里理解成window.foo执行,如果foolet/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();
foo();
obj2.foo();
复制代码


obj2.foo指向了obj.foo的堆内存,此后执行与obj无关(除非使用call/apply改变this指向)


答案


1 
2 
3
复制代码


题目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)
复制代码


用函数预编译的知识来解答这个问题:函数预编译四部曲前两步分别是:


  1. 找形参和变量声明,值赋予undefined
  2. 将形参与实参相统一,也就是将实参的值赋予形参。


obj.foo作为实参,在预编译时将其值赋值给形参fn,是将obj.foo指向的地址赋给了fn,此后fn执行不会与obj产生任何关系。fn为默认绑定。


答案


Window {…}
2
复制代码


题目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的为隐式绑定,thisobj2,打印{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
复制代码


所以如果我们想在setTimeoutsetInterval中使用外界的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();                        
obj.B(function(){           
    console.log(this.name); 
});                         
obj.C();                    
console.log(name);   
复制代码


本题目不做解析,具体可以参照上面的题目。


答案


javascriptthis
javascriptthis
javascriptthis
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): 显示绑定,foothis指向obj
  • foo.apply(obj): 显式绑定
  • foo.bind(obj): 显式绑定,但不会立即执行函数,没有返回值


答案


2
1
1
复制代码


题目4.2:隐式绑定丢失


题目3.4发生隐式绑定的丢失,如下代码:我们可不可以通过显式绑定来修正这个问题。


function foo() {
  console.log(this.a)
}
function doFoo(fn) {
  console.log(this)
  fn()
}
var obj = { a: 1, foo }
var a = 2
doFoo(obj.foo)
复制代码


  1. 首先先修正doFoo()函数的this指向。


doFoo.call(obj, obj.foo)
复制代码


  1. 然后修正fnthis


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()
复制代码


乍一看上去,这个题看起来有些莫名其妙,setTimeout那是传了个什么东西?


做题之前,先了解一下setTimeout的内部机制:(关于异步的执行顺序,可以参考JavaScript之EventLoop)


setTimeout(fn) {
    if (回调条件满足) (
        fn
    )
}
复制代码


这样一看,本题就清楚多了,类似题目4.2,修正了回调函数内fnthis指向。


答案


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()执行的返回值执行callfoo返回值为undefined,执行call()会报错


答案


2
1
2
Uncaught TypeError: Cannot read property 'call' of undefined
复制代码


题目4.5:注意call位置(2)


上面由于foo没有返回函数,无法执行call函数报错,因此修改一下foo函数,让它返回一个函数。


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,返回匿名函数通过callthis指向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(): 默认绑定,打印2
  • foo.bind(obj): 返回新函数,不会执行函数,无输出
  • foo().bind(obj): 第一层foo(),默认绑定,打印2,后bindfoo()返回的匿名函数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通过callthis指向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:window
  • obj.foo.call(obj2)(): 类似题目4.7,第一层obj.foo.call(obj2)使用callobj.foothis指向obj2,打印foo: obj2;第二层匿名函数默认绑定,打印inner:window
  • obj.foo().call(obj2): 类似题目4.5,第一层隐式绑定,打印:foo: obj,第二层匿名函数使用callthis指向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
复制代码


麻了吗,兄弟们。进度已经快过半了,休息一会,争取把this一次性吃透。


image.png

相关文章
|
11天前
|
前端开发 JavaScript 网络协议
前端最常见的JS面试题大全
【4月更文挑战第3天】前端最常见的JS面试题大全
30 5
|
1月前
|
JavaScript 前端开发
javascript中的this
javascript中的this
|
3月前
|
JavaScript 前端开发
错综复杂的this:理清你的JavaScript代码中的指向问题
错综复杂的this:理清你的JavaScript代码中的指向问题
|
1月前
|
JavaScript
JS中改变this指向的六种方法
JS中改变this指向的六种方法
|
25天前
|
JavaScript 前端开发
js开发:请解释this关键字在JavaScript中的用法。
JavaScript中的`this`关键字根据执行上下文指向不同对象:全局作用域中指向全局对象(如`window`),普通函数中默认指向全局对象,但作为对象方法时指向该对象。在构造函数中,`this`指向新实例。箭头函数不绑定`this`,而是继承上下文的`this`值。可通过`call`、`apply`、`bind`方法显式改变`this`指向。
8 2
|
1月前
|
JavaScript
JS中call()、apply()、bind()改变this指向的原理
JS中call()、apply()、bind()改变this指向的原理
|
1月前
|
设计模式 JavaScript 前端开发
最常见的26个JavaScript面试题和答案
最常见的26个JavaScript面试题和答案
38 1
|
2月前
|
JavaScript 前端开发
JavaScript中this的指向问题
JavaScript中this的指向问题
|
3月前
|
前端开发 JavaScript
揭开`this`的神秘面纱:探索 JavaScript 中的上下文密钥(下)
揭开`this`的神秘面纱:探索 JavaScript 中的上下文密钥(下)
|
3月前
|
前端开发 JavaScript
揭开`this`的神秘面纱:探索 JavaScript 中的上下文密钥(上)
揭开`this`的神秘面纱:探索 JavaScript 中的上下文密钥(上)