主頁 > 前端設計 > ?2w字大章 38道面試題 讓你徹底學會JavaScript的this指向問題?

?2w字大章 38道面試題 讓你徹底學會JavaScript的this指向問題?

2021-10-15 22:36:08 前端設計

前言

當一個函式呼叫時,會創建一個執行背景關系,這個背景關系包括函式呼叫的一些資訊(呼叫堆疊,傳入引數,呼叫方式),this就指向這個執行背景關系,

this不是靜態的,也并不是在撰寫的時候系結的,而是在運行時系結的,它的系結和函式宣告的位置沒有關系,只取決于函式呼叫的方式,

本篇文章有點長,涉及到很多道面試題,有難有簡單,如果能耐心的通讀一編,我相信以后this都不成問題,

學習this之前,建議先學習以下知識:

  • JavaScript之預編譯
  • JavaScript之手撕new
  • JavaScript之手撕call/apply
  • JavaScript之靜態作用域與動態作用域
  • JavaScript之手撕陣列高階函式

在文章的最開始,陳列一下本篇文章涉及的內容,保證讓大家不虛此行,

  • 默認系結
  • 隱式系結
  • 隱式系結丟失
  • 顯式系結
  • 顯式系結應用
  • 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) // 2
  }
  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
undefined

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一次性吃透,
在這里插入圖片描述

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

關于陣列高階函式的知識可以參考: JavaScript之手撕高階陣列函式

6.new系結

使用new來構建函式,會執行如下四部操作:

  1. 創建一個空的簡單JavaScript物件(即{});
  2. 為步驟1新創建的物件添加屬性__proto__,將該屬性鏈接至建構式的原型物件 ;
  3. 將步驟1新創建的物件作為this的背景關系 ;
  4. 如果該函式沒有回傳物件,則回傳this

關于new更詳細的知識,可以參考:JavaScript之手撕new

通過new來呼叫建構式,會生成一個新物件,并且把這個新物件系結為呼叫函式的this,

題目6.1: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,列印zc
  • zc.howOld()(): zc.howOld()回傳一個匿名函式,匿名函式為默認系結,因此列印18(阿包永遠18)

答案

zc
18

題目6.3:new界的天王山

new界的天王山,每次看懂后,沒過多久就會忘掉,但這次要從根本上弄清楚該題,

接下來一起來品味品味:

function Foo(){
    getName = function(){ console.log(1); };
    return this;
}
Foo.getName = function(){ console.log(2); };
Foo.prototype.getName = function(){ console.log(3); };
var getName = function(){ console.log(4); };
function getName(){ console.log(5) };

Foo.getName();         
getName();        
Foo().getName();
getName();        
new Foo.getName();
new Foo().getName();
new new Foo().getName();
  1. 預編譯
GO = {
    Foo: fn(Foo),
    getName: function getName(){ console.log(5) };
}
  1. 分析后續執行
  • Foo.getName(): 執行Foo上的getName方法,列印2
  • getName(): 執行GO中的getName方法,列印4
  • Foo().getName()
    • Foo()執行
    // 修改全域GO的getName為function(){ console.log(1); }
    getName = function(){ console.log(1) }
    // Foo為默認系結,this -> window
    // return window
    return this
    
    • Foo().getName(): 執行window.getName(),列印1
  • getName(): 執行GO中的getName,列印1
  1. 分析后面三個列印結果之前,先補充一些運算子優先級方面的知識(圖源:MDN)
    在這里插入圖片描述

    從上圖可以看到,部分優先級如下:new(帶引數串列) = 成員訪問 = 函式呼叫 > new(不帶引數串列)

  2. new Foo.getName()

首先從左往右看:new Foo屬于不帶引數串列的new(優先級19),Foo.getName屬于成員訪問(優先級20),getName()屬于函式呼叫(優先級20),同樣優先級遵循從左往右執行,

  • Foo.getName執行,獲取到Foo上的getName屬性
  • 此時原運算式變為new (Foo.getName)()new (Foo.getName)()為帶引數串列(優先級20),(Foo.getName)()屬于函式呼叫(優先級20),從左往右執行
  • new (Foo.getName)()執行,列印2,并回傳一個以Foo.getName()為建構式的實體

這里有一個誤區:很多人認為這里的new是沒做任何操作的的,執行的是函式呼叫,那么如果執行的是Foo.getName(),呼叫回傳值為undefinednew undefined會發生報錯,并且我們可以驗證一下該運算式的回傳結果,

console.log(new Foo.getName())
// 2
// Foo.getName {}

可見在成員訪問之后,執行的是帶引數串列格式的new操作,

  1. new Foo().getName()
    • 步驟4一樣分析,先執行new Foo(),回傳一個以Foo為建構式的實體
    • Foo的實體物件上沒有getName方法,沿原型鏈查找到Foo.prototype.getName方法,列印2
  2. new new Foo().getName()

從左往右分析: 第一個new不帶引數串列(優先級19),new Foo()帶引數串列(優先級20),剩下的成員訪問和函式呼叫優先級都是20

  • new Foo()執行,回傳一個以Foo為建構式的實體
  • 在執行成員訪問,Foo實體物件在Foo.prototype查找到getName屬性
  • 執行new (new Foo().getName)(),回傳一個以 Foo.prototype.getName()為建構式的實體,列印2
  1. new Foo.getName()new new Foo().getName()區別:
  • new Foo.getName()的建構式是Foo.getName
  • new new Foo().getName()的建構式為Foo.prototype.getName

測驗結果如下:

foo1 = new Foo.getName()
foo2 = new new Foo().getName()
console.log(foo1.constructor)
console.log(foo2.constructor)

輸出結果:

2
3
? (){ console.log(2); }
? (){ console.log(3); }

通過這一步比較應該能更好的理解上面的執行順序,

答案

2
4
1
1
2
3
3

兄弟們,革命快要成功了,再努力一把,以后this都小問題啦,
在這里插入圖片描述

7.箭頭函式

箭頭函式沒有自己的this,它的this指向外層作用域的this,且指向函式定義時的this而非執行時,

  1. this指向外層作用域的this: 箭頭函式沒有this系結,但它可以通過作用域鏈查到外層作用域的this
  2. 指向函式定義時的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 tom
  • obj.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,obj1
  • obj2.intro()(): obj2.intro()為箭頭函式,this為外層作用域this,指向window,回傳匿名函式為默認系結,列印window,window
  • obj3.intro()(): obj3.intro()obj2.intro()相同,回傳值為箭頭函式,外層作用域introthis指向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();
  • zcnew User實體,因此建構式Userthis指向zc
  • zc.intro(): 列印My name is zc
  • zc.howOld(): howOld為箭頭函式,箭頭函式this由外層作用域決定,且指向函式定義時的this,外層作用域為Userthis指向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修改thisobj2,列印obj2,第二層函式為箭頭函式,它的this與外層this相同,同樣列印obj2
  • obj1.intro().call(obj2): 第一層函式列印obj1,第二次函式為箭頭函式,call無效,它的this與外層this相同,列印obj1
  • obj1.intro2.call(obj2)(): 第一層為箭頭函式,call無效,外層作用域為window,列印window;第二次為普通匿名函式,默認系結,列印window
  • obj1.intro2().call(obj2): 與上同,列印window;第二層為匿名函式,call修改thisobj2,列印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屬性,

避免使用場景

  1. 箭頭函式定義物件方法
const zc = {
    name: 'zc',
    intro: () => {
        // this -> window
        console.log(this.name)
    }
}
zc.intro() // undefined
  1. 箭頭函式不能作為建構式
const User = (name, age) => {
    this.name = name;
    this.age = age;
}
// Uncaught TypeError: User is not a constructor
zc = new User('zc', 24);
  1. 事件的回呼函式
    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): 箭頭函式與call
  • user1.foo3()()、user1.foo3.call(user2)()、user1.foo3().call(user2): 見題目4.8
  • user1.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](): 這種執行方式看起來就怪怪的,咱們把它展開來看看:

    1. arguments是一個類陣列,arguments展開,應該是下面這樣:
    arguments: {
        0: fn,
        1: 1,
        length: 2
    }
    
    1. arguments[0]: 這是訪問物件的屬性0?0不好理解,咱們把它稍微一換,方便一下理解:
    arguments: {
        fn: fn,
        1: 1,
        length: 2
    }
    
    1. 到這里大家應該就懂了,隱式系結,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()

  1. obj.fn為立即執行函式: 默認系結,this指向window

    我們來一句一句的分析:

    • var number: 立即執行函式的AO中添加number屬性,值為undefined
    • this.number *= 2: window.number = 10
    • number = number * 2: 立即執行函式AOnumber值為undefined,賦值后為NaN
    • number = 3: AOnumber值由NaN修改為3
    • 回傳匿名函式,形成閉包

    此時的obj可以類似的看成以下代碼(注意存在閉包):

    obj = {
       number: 3,
       fn: function () {
            var num = this.number;
            this.number *= 2;
            console.log(num);
            number *= 3;
            console.log(number);
        }
    }
    
  2. myFun.call(null): 相當于myFun(),隱式系結丟失,myFunthis指向window

    依舊一句一句的分析:

    • var num = this.number: this指向windownum = window.num = 10
    • this.number *= 2: window.number = 20
    • console.log(num): 列印10
    • number *= 3: 當前AO中沒有number屬性,沿作用域鏈可在立即執行函式的AO中查到number屬性,修改其值為9
    • console.log(number): 列印立即執行函式AO中的number,列印9
  3. obj.fn(): 隱式系結,fnthis指向obj

    繼續一步一步的分析:

    • var num = this.number: this->objnum = obj.num = 3
    • this.number *= 2: obj.number *= 2 = 6
    • console.log(num): 列印num值,列印3
    • number *= 3: 當前AO中不存在number,繼續修改立即執行函式AO中的numbernumber *= 3 = 27
    • console.log(number): 列印27
  4. console.log(window.number): 列印20

這里解釋一下,為什么myFun.call(null)執行時,找不到number變數,是去找立即執行函式AO中的number,而不是找window.number: JavaScript采用的靜態作用域,當定義函式后,作用域鏈就已經定死,(更詳細的解釋文章最開始的推薦中有)

答案

10
9
3
27
20

總結

  • 默認系結: 非嚴格模式下this指向全域物件,嚴格模式下this會系結到undefined
  • 隱式系結: 滿足XXX.fn()格式,fnthis指向XXX,如果存在鏈式呼叫,this永遠指向最后呼叫它的那個物件
  • 隱式系結丟失:起函式別名,通過別名運行;函式作為引數會造成隱式系結丟失,
  • 顯示系結: 通過call/apply/bind修改this指向
  • new系結: 通過new來呼叫建構式,會生成一個新物件,并且把這個新物件系結為呼叫函式的this
  • 箭頭函式系結: 箭頭函式沒有this,它的this是通過作用域鏈查到外層作用域的this,且指向函式定義時的this而非執行時

后語

this到這里基本接近尾聲了,松了一口氣,
這篇文章寫了好久,找資源,修改博文,各種亂七八糟的雜事,導致遲遲寫不出滿意的博文,有可能天生理科男的緣故吧,怎么寫感覺文章都很生硬,但好在還是順利寫完了,

在文章的最后,感謝一下參考的博客和題目的來源

  • 霖呆呆大佬
  • 小夕大佬:嗨,你真的懂this嗎?
  • 渡一教育的題源

最后按照阿包慣例,附贈一道面試題:

var num = 10
var obj = {num: 20}
obj.fn = (function (num) {
  this.num = num * 3
  num++
  return function (n) {
    this.num += n
    num++
    console.log(num)
  }
})(obj.num)
var fn = obj.fn
fn(5)
obj.fn(10)
console.log(num, obj.num)

最后祝大家都能學好前端,步步登神,成為大佬,
在這里插入圖片描述

轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/316668.html

標籤:其他

上一篇:跨域問題的解決方案(還在苦苦求后端小伙伴寫cors嗎? 看完站起來,翻身做主人!!)

下一篇:常用滑鼠 、鍵盤事件及事件物件

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • vue移動端上拉加載

    可能做得過于簡單或者比較low,請各位大佬留情,一起探討技術 ......

    uj5u.com 2020-09-10 04:38:07 more
  • 優美網站首頁,頂部多層導航

    一個個人用的瀏覽器首頁,可以把一下常用的網站放在這里,平常打開會比較方便。 第一步,HTML代碼 <script src=https://www.cnblogs.com/szharf/p/"js/jquery-3.4.1.min.js"></script> <div id="navigate"> <ul> <li class="labels labels_1"> ......

    uj5u.com 2020-09-10 04:38:47 more
  • 頁面為要加<!DOCTYPE html>

    最近因為寫一個js函式,需要用到$(window).height(); 由于手寫demo的時候,過于自信,其實對前端方面的認識也不夠體系,用文本檔案直接敲出來的html代碼,第一行沒有加上<!DOCTYPE html> 導致了$(window).height();的結果直接是整個document的高 ......

    uj5u.com 2020-09-10 04:38:52 more
  • WordPress網站程式手動升級要做好資料備份

    WordPress博客網站程式在進行升級前,必須要做好網站資料的備份,這個問題良家佐言是遇見過的;在剛開始接觸WordPress博客程式的時候,因為升級問題和博客網站的修改的一些嘗試,良家佐言是吃盡了苦頭。因為購買的是西部數碼的空間和域名,每當佐言把自己的WordPress博客網站搞到一塌糊涂的時候 ......

    uj5u.com 2020-09-10 04:39:30 more
  • WordPress程式不能升級為5.4.2版本的原因

    WordPress是一款個人博客系統,受到英文博客愛好者和中文博客愛好者的追捧,并逐步演化成一款內容管理系統軟體;它是使用PHP語言和MySQL資料庫開發的,用戶可以在支持PHP和MySQL資料庫的服務器上使用自己的博客。每一次WordPress程式的更新,就會牽動無數WordPress愛好者的心, ......

    uj5u.com 2020-09-10 04:39:49 more
  • 使用CSS3的偽元素進行首字母下沉和首行改變樣式

    網頁中常見的一種效果,首字改變樣式或者首行改變樣式,效果如下圖。 代碼: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, ......

    uj5u.com 2020-09-10 04:40:09 more
  • 關于a標簽的講解

    什么是a標簽? <a> 標簽定義超鏈接,用于從一個頁面鏈接到另一個頁面。 <a> 元素最重要的屬性是 href 屬性,它指定鏈接的目標。 a標簽的語法格式:<a href=https://www.cnblogs.com/summerxbc/p/"指定要跳轉的目標界面的鏈接">需要展示給用戶看見的內容</a> a標簽 在所有瀏覽器中,鏈接的默認外觀如下: 未被訪問的鏈接帶 ......

    uj5u.com 2020-09-10 04:40:11 more
  • 前端輪播圖

    在需要輪播的頁面是引入swiper.min.js和swiper.min.css swiper.min.js地址: 鏈接:https://pan.baidu.com/s/15Uh516YHa4CV3X-RyjEIWw 提取碼:4aks swiper.min.css地址 鏈接:https://pan.b ......

    uj5u.com 2020-09-10 04:40:13 more
  • 如何設定html中的背景圖片(全屏顯示,且不拉伸)

    1 <style>2 body{background-image:url(https://uploadbeta.com/api/pictures/random/?key=BingEverydayWallpaperPicture); 3 background-size:cover;background ......

    uj5u.com 2020-09-10 04:40:16 more
  • Java學習——HTML詳解(上)

    HTML詳解 初識HTML Hyper Text Markup Language(超文本標記語言) 1 <!--DOCTYPE:告訴瀏覽器我們要使用什么規范--> 2 <!DOCTYPE html> 3 <html lang="en"> 4 <head> 5 <!--meta 描述性的標簽,描述一些 ......

    uj5u.com 2020-09-10 04:40:33 more
最新发布
  • 我的第一個NPM包:panghu-planebattle-esm(胖虎飛機大戰)使用說明

    好家伙,我的包終于開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ......

    uj5u.com 2023-04-20 07:59:23 more
  • 生產事故-走近科學之消失的JWT

    入職多年,面對生產環境,盡管都是小心翼翼,慎之又慎,還是難免捅出簍子。輕則滿頭大汗,面紅耳赤。重則系統停擺,損失資金。每一個生產事故的背后,都是寶貴的經驗和教訓,都是專案成員的血淚史。為了更好地防范和遏制今后的各類事故,特開此專題,長期更新和記錄大大小小的各類事故。有些是親身經歷,有些是經人耳傳口授 ......

    uj5u.com 2023-04-18 07:55:04 more
  • 記錄--Canvas實作打飛字游戲

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 打開游戲界面,看到一個畫面簡潔、卻又富有挑戰性的游戲。螢屏上,有一個白色的矩形框,里面不斷下落著各種單詞,而我需要迅速地輸入這些單詞。如果我輸入的單詞與螢屏上的單詞匹配,那么我就可以獲得得分;如果我輸入的單詞錯誤或者時間過長,那么我就會輸 ......

    uj5u.com 2023-04-04 08:35:30 more
  • 了解 HTTP 看這一篇就夠

    在學習網路之前,了解它的歷史能夠幫助我們明白為何它會發展為如今這個樣子,引發探究網路的興趣。下面的這張圖片就展示了“互聯網”誕生至今的發展歷程。 ......

    uj5u.com 2023-03-16 11:00:15 more
  • 藍牙-低功耗中心設備

    //11.開啟藍牙配接器 openBluetoothAdapter //21.開始搜索藍牙設備 startBluetoothDevicesDiscovery //31.開啟監聽搜索藍牙設備 onBluetoothDeviceFound //30.停止監聽搜索藍牙設備 offBluetoothDevi ......

    uj5u.com 2023-03-15 09:06:45 more
  • canvas畫板(滑鼠和觸摸)

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>canves</title> <style> #canvas { cursor:url(../images/pen.png),crosshair; } #canvasdiv{ bo ......

    uj5u.com 2023-02-15 08:56:31 more
  • 手機端H5 實作自定義拍照界面

    手機端 H5 實作自定義拍照界面也可以使用 MediaDevices API 和 <video> 標簽來實作,和在桌面端做法基本一致。 首先,使用 MediaDevices.getUserMedia() 方法獲取攝像頭媒體流,并將其傳遞給 <video> 標簽進行渲染。 接著,使用 HTML 的 < ......

    uj5u.com 2023-01-12 07:58:22 more
  • 記錄--短視頻滑動播放在 H5 下的實作

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 短視頻已經無數不在了,但是主體還是使用 app 來承載的。本文講述 H5 如何實作 app 的視頻滑動體驗。 無聲勝有聲,一圖頂百辯,且看下圖: 網址鏈接(需在微信或者手Q中瀏覽) 從上圖可以看到,我們主要實作的功能也是本文要講解的有: ......

    uj5u.com 2023-01-04 07:29:05 more
  • 一文讀懂 HTTP/1 HTTP/2 HTTP/3

    從 1989 年萬維網(www)誕生,HTTP(HyperText Transfer Protocol)經歷了眾多版本迭代,WebSocket 也在期間萌芽。1991 年 HTTP0.9 被發明。1996 年出現了 HTTP1.0。2015 年 HTTP2 正式發布。2020 年 HTTP3 或能正... ......

    uj5u.com 2022-12-24 06:56:02 more
  • 【HTML基礎篇002】HTML之form表單超詳解

    ??一、form表單是什么

    ??二、form表單的屬性

    ??三、input中的各種Type屬性值

    ??四、標簽 ......

    uj5u.com 2022-12-18 07:17:06 more