js中this/作用域

this 問(wèn)題總結(jié)

默認(rèn)綁定(函數(shù)直接調(diào)用)

非嚴(yán)格模式下:

function fn() {
  console.log(this) // 打印window 對(duì)象
}

fn() 

嚴(yán)格模式下:

function fn() {
  'use strict'
  console.log(this) // 打印undefined
}

fn()

TIP1 ?? 非嚴(yán)格模式下,默認(rèn)綁定指向全局(node 中式 global

var a = 1
function fn(){
    var a = 2
    console.log(this.a) // 1
}
fn()

var b = 1
function outer () {
  var b = 2
  function inner () { 
    console.log(this.b) // 1
  }
  inner()
}

outer()


const obj = {
  a: 1,
  fn: function() {
    console.log(this.a)
  }
}

obj.fn() // 1
const f = obj.fn
f() // undefined

隱式綁定(屬性訪問(wèn)調(diào)用)

function fn () {
  console.log(this.a)
}

const obj = {
  a: 1
}

obj.fn = fn
obj.fn() // 1

TIP ?? 隱式綁定的 this 指的是調(diào)用堆棧的上一級(jí).前面個(gè))

function fn () {
  console.log(this.a)
}

const obj1 = {
  a: 1,
  fn
}

const obj2 = {
  a: 2,
  obj1
}

obj2.obj1.fn() // 1

面試官一般問(wèn)的是一些邊界 case,比如隱式綁定失效(列舉部分):

// 第一種 是前面提過(guò)的情況
const obj1 = {
  a: 1,
  fn: function() {
    console.log(this.a)
  }
}

const fn1 = obj1.fn // 將引用給了 fn1,等同于寫了 function fn1() { console.log(this.a) }
fn1() // 所以這里其實(shí)已經(jīng)變成了默認(rèn)綁定規(guī)則了,該函數(shù) `fn1` 執(zhí)行的環(huán)境就是全局環(huán)境

// 第二種 setTimeout
setTimeout(obj1.fn, 1000) // 這里執(zhí)行的環(huán)境同樣是全局

// 第三種 函數(shù)作為參數(shù)傳遞
function run(fn) {
  fn()
}
run(obj1.fn) // 這里傳進(jìn)去的是一個(gè)引用

// 第四種 一般匿名函數(shù)也是會(huì)指向全局的
var name = 'The Window';
var obj = {
    name: 'My obj',
    getName: function() {
        return function() { // 這是一個(gè)匿名函數(shù)
            console.log(this.name)
        };
    }
}
obj.getName()()

// 第五種 函數(shù)賦值也會(huì)改變 this 指向,下邊練習(xí)題會(huì)有 case,react 中事件處理函數(shù)為啥要 bind 一下的原因
// 第六種 IIFE

顯式綁定(callbind、 apply

通過(guò)顯式的一些方法去強(qiáng)行的綁定 this 上下文

function fn () {
  console.log(this.a)
}

const obj = {
  a: 100
}

fn.call(obj) // 100

TIP ?? 這種根本還是取決于第一個(gè)參數(shù)
但是第一個(gè)為 null 的時(shí)候還是綁到全局的

function fn(){
    console.log(this)
}
// boxing(裝箱) -> (1 ----> Number(1))
// bind 只看第一個(gè) bind(堆棧的上下文,上一個(gè),寫的順序來(lái)看就是第一個(gè))
fn.bind(1).bind(2)()  // Number {1}

js bind 的實(shí)現(xiàn)(FROM MDN):

//  Yes, it does work with `new (funcA.bind(thisArg, args))`
if (!Function.prototype.bind) (function(){
  var ArrayPrototypeSlice = Array.prototype.slice; // 為了 this
  Function.prototype.bind = function(otherThis) {
    // 調(diào)用者必須是函數(shù),這里的 this 指向調(diào)用者:fn.bind(ctx, ...args) / fn
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

    var baseArgs= ArrayPrototypeSlice.call(arguments, 1), // 取余下的參數(shù)
        baseArgsLength = baseArgs.length,
        fToBind = this, // 調(diào)用者
        fNOP    = function() {}, // 寄生組合集成需要一個(gè)中間函數(shù),避免兩次構(gòu)造
        fBound  = function() {
          // const newFn = fn.bind(ctx, 1); newFn(2) -> arguments: [1, 2]
          baseArgs.length = baseArgsLength; // reset to default base arguments
          baseArgs.push.apply(baseArgs, arguments); // 參數(shù)收集
          return fToBind.apply( // apply 顯示綁定 this
            // 判斷是不是 new 調(diào)用的情況,這里也說(shuō)明了后邊要講的優(yōu)先級(jí)問(wèn)題      
            fNOP.prototype.isPrototypeOf(this) ? this : otherThis, baseArgs
          );
        };
        // 下邊是為了實(shí)現(xiàn)原型繼承
    if (this.prototype) { // 函數(shù)的原型指向其構(gòu)造函數(shù),構(gòu)造函數(shù)的原型指向函數(shù)
      // Function.prototype doesn't have a prototype property
      fNOP.prototype = this.prototype; // 就是讓中間函數(shù)的構(gòu)造函數(shù)指向調(diào)用者的構(gòu)造
    }
    fBound.prototype = new fNOP(); // 繼承中間函數(shù),其實(shí)這里也繼承了調(diào)用者了

    return fBound; // new fn()
  };
})();

TIP ?? 如果函數(shù) constructor 里沒(méi)有返回對(duì)象的話,this 指向的是 new 之后得到的實(shí)例

function foo(a) {
  this.a = a
}

const f = new foo(2)
f.a // 2

function bar(a) {
  this.a = a
  return {
    a: 100
  }
}
const b = new bar(3)
b.a // 100

箭頭函數(shù)

箭頭函數(shù)這種情況比較特殊,編譯期間確定的上下文,不會(huì)被改變,哪怕你 new,指向的就是上一層的上下文,

TIP ?? 箭頭函數(shù)本身是沒(méi)有 this 的,繼承的是外層的

function fn() {
  return {
    b: () => {
      console.log(this)
    }
  }
}

fn().b() // window       與 fn 的this 指向一致
fn().b.bind(1)() // window      與 fn 的this 指向一致
fn.bind(2)().b.bind(3)() // Number{2}      與 fn 的this 指向一致

function fn() {
  return {
    b: function() {
      console.log(this)
    }
  }
}
fn().b() // {b: ?}    指向 函數(shù)b自己的this
fn().b.bind(1)() // Number{1}
fn.bind(2)().b.bind(3)() // Number{3}

優(yōu)先級(jí)

這上面的各種方式一定是有先后順序的,同時(shí)作用于一個(gè)函數(shù)的時(shí)候,以哪一個(gè)為準(zhǔn)呢?這取決于優(yōu)先級(jí)

// 隱式 vs 默認(rèn) -> 結(jié)論:隱式 > 默認(rèn)
function fn() {
  console.log(this)
}

const obj = {
  fn
}

obj.fn() //{fn: ?}

// 顯式 vs 隱式 -> 結(jié)論:顯式 > 隱式
obj.fn.bind(5)() // Number {5}

// new vs 顯式 -> 結(jié)論:new > 顯式
function foo (a) {
    this.a = a
}

const obj1 = {}

var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a) // 2

// new
var baz = new bar(3)
console.log( obj1.a ) // 2
console.log( baz.a ) // 3

// 箭頭函數(shù)沒(méi)有 this,比較沒(méi)有意義
// 1.
function foo() {
  console.log( this.a ) // 2
}
var a = 2;
(function(){
  "use strict" // 迷惑大家的
  foo();    // 直接函數(shù)執(zhí)行this 指向window
})();

// 2.
var name="the window"

var object={
  name:"My Object", 
  getName: function(){ 
    return this.name
  } 
}
object.getName() // "My Object"
(object.getName)() // "My Object"
(object.getName = object.getName)() // "the window"
(object.getName, object.getName)() // "the window"

// 3.
var x = 3
var obj3 = {
  x: 1,
  getX: function() {
    var x = 5
    return function() {
      return this.x
    }(); // ??
  }
}
console.log(obj3.getX()) // 3

// 4. 
function a(x){
  this.x = x
  return this
}
var x = a(5) // 替換為 let 再試試
var y = a(6) // 替換為 let 再試試 // 再換回 var,但是去掉 y 的情況,再試試

console.log(x.x) // undefine
console.log(y.x) // 6

// 等價(jià)于
window.x = 5;
window.x = window;

window.x = 6;
window.y = window;

console.log(x.x) // void 0 其實(shí)執(zhí)行的是 Number(6).x
console.log(y.x) // 6

作用域和閉包

存儲(chǔ)空間、執(zhí)行上下文

?? 數(shù)據(jù)是怎么存的?

本質(zhì)是將數(shù)據(jù)映射成 0 1 ,然后通過(guò)觸發(fā)器存儲(chǔ)這類信息(電信號(hào))

?? 棧 和 堆 / 靜態(tài)內(nèi)存分配 和 動(dòng)態(tài)內(nèi)存分配

堆棧這里指的是存儲(chǔ)數(shù)據(jù)結(jié)構(gòu),當(dāng)然本身也可以是一種數(shù)據(jù)結(jié)構(gòu)的概念(二叉堆、棧)

靜態(tài)內(nèi)存分配:

  • 編譯期知道所需內(nèi)存空間大小。
  • 編譯期執(zhí)行
  • 申請(qǐng)到??臻g
  • FILO(先進(jìn)后出)

動(dòng)態(tài)內(nèi)存分配:

  • 編譯期不知道所需內(nèi)存空間大小
  • 運(yùn)行期執(zhí)行
  • 申請(qǐng)到堆空間
  • 沒(méi)有特定的順序

當(dāng)控制器轉(zhuǎn)到一段可執(zhí)行代碼的時(shí)候就會(huì)進(jìn)入到一個(gè)執(zhí)行上下文。執(zhí)行上下文是一個(gè)堆棧結(jié)構(gòu)(先進(jìn)后出), 棧底部永遠(yuǎn)是全局上下文,棧頂是當(dāng)前活動(dòng)的上下文。其余都是在等待的狀態(tài),這也印證了JS中函數(shù)執(zhí)行的原子性

可執(zhí)行代碼與執(zhí)行上下文是相對(duì)的,某些時(shí)刻二者等價(jià)

可執(zhí)行代碼(大致可以這么劃分):

  • 全局代碼
  • 函數(shù)
  • eval

執(zhí)行上下文(簡(jiǎn)稱 EC)中主要分為三部分內(nèi)容:

  • VO / AO 變量對(duì)象
  • 作用域鏈
  • This
    所以這個(gè)流程可以梳理出來(lái):
  1. 遇到可執(zhí)行代碼

  2. 創(chuàng)建一個(gè)執(zhí)行上下文 (可執(zhí)行代碼的生命周期:編譯、運(yùn)行)

    2.1 初始化 VO

    2.2 建立作用域鏈

    2.3 確定 This 上下文

  3. 可執(zhí)行代碼執(zhí)行階段

    3.1 參數(shù)、變量賦值、提升

    3.2 函數(shù)引用

    3.3 ...

  4. 出棧

?? 作用域鏈

每一個(gè)執(zhí)行上下文都與一個(gè)作用域鏈相關(guān)聯(lián)。作用域鏈?zhǔn)且粋€(gè)對(duì)象組成的鏈表,求值標(biāo)識(shí)符的時(shí)候會(huì)搜索它。當(dāng)控制進(jìn)入執(zhí)行上下文時(shí),就根據(jù)代碼類型創(chuàng)建一個(gè)作用域鏈,并用初始化對(duì)象(VO/AO)填充。執(zhí)行一個(gè)上下文的時(shí)候,其作用域鏈只會(huì)被 with 聲明和 catch 語(yǔ)句所影響

var a = 20;
function foo(){
    var b = 100;
    alert( a + b );
}
foo();

// 兩個(gè)階段:創(chuàng)建 - 執(zhí)行

// --------------------------- 創(chuàng)建 ------------------------------

// 模擬 VO/AO 對(duì)象
AO(foo) {
  b: void 0
}

// [[scope]] 不是作用域鏈,只是函數(shù)的一個(gè)屬性(規(guī)范里的,不是實(shí)際實(shí)現(xiàn))
// 在函數(shù)創(chuàng)建時(shí)被存儲(chǔ),靜態(tài)(不變的),永遠(yuǎn)永遠(yuǎn),直到函數(shù)被銷毀
foo.[[scope]]: [VO(global)]

VO(global) {
  a: void 0,
  foo: Reference<'foo'>
}
  
// --------------------------- 調(diào)用 ------------------------------
  
// 可以這么去理解,近似的用一個(gè) concant 模擬,就是將當(dāng)前的活動(dòng)對(duì)象放作用域鏈最前邊
Scope = [AO|VO].concat([[Scope]])
  
  
// ---------------------------- 執(zhí)行時(shí) EC --------------------------------
EC(global) {
  VO(global) {
    a: void 0,
    foo: Reference<'foo'>
  },
  Scope: [VO(global)],
  // this
}
  
EC(foo) {
  AO(foo) { // 聲明的變量,參數(shù)
    b: void 0
  },
  Scope: [AO(foo), VO(global)] // 查找順序 -> RHS LHS  
}

特殊情況:

  • Function 構(gòu)造的函數(shù) [[scope]] 里只有全局的變量對(duì)象
// 證明
var a = 10;

function foo(){
  var b = 20;
  // 函數(shù)聲明
  function f1(){ // EC(f1) { Scope: [AO(f1), VO(foo), VO(g)] }
    console.log(a, b);
  }

  // 函數(shù)表達(dá)式
  var f2 = function(){
    console.log(a, b);
  }

  var f3 = Function('console.log(a,b)')

  f1(); // 10, 20
  f2(); // 10, 20
  f3(); // 10, b is not defined
}

foo();
  • with & catch & eval

本質(zhì)上 eval 之類的恐怖之處是可以很方便的修改作用域鏈,執(zhí)行完后又回歸最初狀態(tài)

// 這樣好理解
Scope = [ withObj|catchObj ].concat( [ AO|VO ].concat( [[ scope ]] ) )
// 初始狀態(tài) [VO(foo), VO(global)]
// with 一下:[VO(with)?, VO(foo), VO(global)]
// with 完事兒了,還要恢復(fù) ??
var a = 15, b = 15;

with( { a: 10 } ){
  var a = 30, b = 30;
  alert(a); // 30
  alert(b); // 30
}

alert(a); // ? answer: 15
alert(b); // 30
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容