前端日拱一卒D12——js基礎篇

DerekZ95發表於2018-08-08

前言

餘為前端菜鳥,感姿勢水平匱乏,難觀前端之大局。遂決定循前端知識之脈絡,以興趣為引,輔以幾分堅持,望於己能解惑致知、於同道能助力一二,豈不美哉。

本系列程式碼及文件均在 此處

內建型別

分類

  • 基本型別 number, string, boolean, null, undefined, symbol
  • 物件Object

判斷型別

  • typeof

    對於基本型別(null除外)都能得到正常結果

    對於物件(函式除外)都顯示object

  • Object.prototype.toString.call

    正確獲取物件type的方法

    Object.prototype.toString.call([]) // [object Array]
    複製程式碼

型別轉換

  • 顯示轉換

    Number(), Boolean(), String(), Object()

  • 隱式轉換

    基本型別之間的轉換比較簡單,怪異的地方在於物件轉基本型別

    物件轉基本型別發生在一些運算子操作時

    • !操作符將物件轉boolean

      這個是最簡單的,全是true

    • 四則運算

      +:一方為字串則另一方也轉為字串

      [1, 2] + [2, 1] === '1,22,1'
      'a' + +'b' ===' aNaN'
      複製程式碼

      其他:一方為數字則另一方也轉為數字

    • ==

      最鬧心的東西,其實不需要去硬記那些規則,大致如下:

      • 同型別比較不用多說,注意NaN!==NaN, +0 == -0
      • 一方為number或者boolean時,另一方轉為基本型別以number比較
      • 一方為string時,另一方轉為基本型別再參照上一條比較
    • 物件轉基本型別(toPrimitive)

      Symbol.prototype[@@toPrimitive], Object.prototype.toString(), Object.prototype.valueOf()

      以上是物件轉基本型別時呼叫的方法,可以自己重寫

原型和繼承

原型鏈

前端日拱一卒D12——js基礎篇

  • 函式的prototype

    每個函式都有一個原型prototype(Function.prototype.bind()建立的函式除外,因為Function.prototype是內建的東西),函式的prototype初始只有constructor一個屬性,指向函式本身

  • 物件的__proto__

    由建構函式生成的物件會有一個__proto__的屬性,稱為隱式原型,指向了建構函式的原型

    a.__proto__ === A.prototype
    複製程式碼

    我們所說的原型鏈其實就是根據這樣的聯絡構建起來的一個鏈路

  • new關鍵字

    new關鍵字執行的時候實際上做了這麼幾步

    • 建立空物件
    • 繫結原型
    • 繫結this
    • 返回物件
    function create(){
        const obj = new Object()
        // Con為建構函式本身
        const Con = [].shift.call(arguments)
        obj.__proto__ = Con.prototype
        // 繫結this
        const res = Con.apply(obj, arguments)
        return res
    }
    複製程式碼
  • instanceof

    js的繼承實際上不是說根據類的定義將類的屬性copy到例項上,js的繼承實際上是通過__proto__和prototype建立聯絡,使得方法/屬性可以通過原型鏈訪問到。因此要判斷繼承關係還是需要通過原型鏈

    function instanceof(a, b) {
        const proto = b.prototype
        let _proto = a.__proto__
        while(true) {
            // 到頭了
            if (_proto === null) {
                return false
            }
            if (proto === _proto) {
                return true
            }
            _proto = _proto.__proto__
        }
    }
    複製程式碼

繼承

es6的class簡化了繼承的寫法

class A {
    constructor(value) {
        // 生成物件的例項屬性
        this.value = value
    }
    // doA掛在A的prototype上
    doA(){
        console.log(123)
    }
}
class B extends A{
    // 達到重寫的目的,但實際上A的原型上的方法還是在的
    doA(){
        console.log(456)
    }
}
複製程式碼

this,執行上下文與作用域

before it

前端日拱一卒D12——js基礎篇

介紹幾個概念

  • Handle

    記憶體分配在堆(heap)內進行,javaScript的值和物件都在堆內,Handle(控制程式碼)是指向物件的一個指標,所有V8物件都是通過控制程式碼訪問,這樣GC機制才能實現。

    Handle分為Local和Persistent兩種,前者被HandleScope管理,是區域性的,後者不受HandleScope管理,需要Persistent::New, Persistent::Dispose配對使用,類似C++的new和delete。

  • Handle Scope

    HandleScope是Handle的容器,HandleScope生命週期結束時Handle會被釋放,引起heap中物件引用的更新。

  • Context

    Context是javaScript執行的環境,其中包含了javaScript的內建物件和內建函式等。Context可以巢狀,可以從A切換到B。

  • 看點不一樣的hello world

    #include <v8.h>
    using namespace v8;
    int main(int argc, char* argv[]) {
    // 建立一個Handle容器
    HandleScope handle_scope;
    // 建立一個執行環境/上下文
    Persistent<Context> context = Context::New();
    // 進入建立的執行環境
    Context::Scope context_scope(context);
    // 建立String
    Handle<String> source = String::New("'Hello' + ', World!'");
    // 編譯原始碼
    Handle<Script> script = Script::Compile(source);
    // 執行得到結果
    Handle<Value> result = script->Run();
    // 執行環境銷燬
    context.Dispose();
    // 列印結果
    String::AsciiValue ascii(result);
    printf("%s\n", *ascii);
    return 0;
    // handle_scope生命週期結束,handle都被釋放
    }
    複製程式碼

執行上下文

js執行一段可執行程式碼時會產生執行上下文,執行上下文存在棧中

舉個?

function B() {}
function A() {
    B()
}
複製程式碼

like this:

stack = []
stack.push(globalContext) // 全域性Context
stack.push(Acontext) // 執行A函式
stack.push(Bcontext) // 呼叫B函式
stack.pop() // B執行完畢,出棧
stack.pop() // A執行完畢,出棧
複製程式碼

上下文中內容

前端日拱一卒D12——js基礎篇

  • 變數物件VO(variable object)

    變數物件儲存了上下文中定義的變數和宣告,也包含一些內建的物件。

    全域性上下文中的變數物件就是全域性物件,包含有很多內建的物件和方法

    函式上下文中的變數物件又稱活動物件AO(active object),初始時只有Arguments物件

    • 進入執行上下文: 進行函式、變數宣告和形參宣告

      function foo(a) {
          f()
          function f() {
              console.log(123)
          }
          function f() {
              console.log(456)
          }
          var f = 1
      }
      foo(1) // 456
      複製程式碼

      函式優先於變數被提升宣告,同名函式會被覆蓋

      與已有函式/變數同名的變數宣告會被忽略

      此時的變數物件/活動物件為

      AO = {
          arguments: { 0: 1, length: 1},
          a: 1,
          f: reference to function f() {},
      }
      複製程式碼
    • 執行過程

      根據程式碼執行結果修改活動物件的值

  • 作用域鏈[[scope]]

    我們說函式的作用域是在函式定義的時候確定的,那是因為函式都有一個內部屬性[[scope]],包含了父級變數物件

    function foo() {
        function bar() {
    
        }
    }
    複製程式碼

    此時兩個函式的[[scope]]:

    foo.[[scope]] = [globalContext.VO]
    bar.[[scope]] = [globalContext.VO, fooContext.AO]
    複製程式碼

    函式執行時,進入對應上下文,會將該上下文中的AO加到[[scope]]的前面

    bar()
    // AO為bar執行上下文中的活躍物件
    // Scope為bar執行上下文的作用域鏈
    Scope = [AO].concat(bar.[[scope]])
    複製程式碼

    而這個Scope就是我們查詢變數值的作用域鏈

    舉個?

    var a = 'outside'
    function foo() {
        var a = 'inside'
        return a
    }
    foo()
    複製程式碼

    步驟如下

    // 1.建立foo函式
    foo.[[scope]] = [globalContext.VO]
    // 2.執行foo函式
    stack = [globalContext, fooContext] // 上下文壓棧
    // 3.複製函式[[scope]]建立作用域鏈
    fooContext.Scope = fooContext.[[scope]]
    // 4.函式/變數宣告 -> 活動物件建立
    AO = { arguments: { length: 0 }, a: undefined }
    // 5.將AO加到作用域鏈頂端
    fooContext.Scope = [AO, fooContext.[[scope]]]
    // 6.函式執行修改AO值
    AO = { arguments: { length: 0 }, a: 'inside' }
    // 7.函式返回,fooContext出棧
    stack = [globalContext]
    複製程式碼
  • this

    this的指向規則其實很簡單,分為以下幾種情況:

    • 全域性/普通呼叫 -----> 頂層物件/undefined(嚴格模式)
    • 存在引用,即函式作為物件屬性被直接呼叫 -----> 該物件
    • 建構函式生成 -----> 生成物件
    • call, apply. bind 強制改變this指向

    然而...this賦值的原理,並不是很懂啊...很難受

小結

函式建立時生成詞法作用域[[scope]],執行時建立執行上下文並壓棧,執行上下文建立時需要複製函式的作用域建立作用域鏈。

然後進行函式/變數的宣告和形參宣告(根據arguments物件生成),根據上述物件生成活動物件,並將活動物件加到作用域鏈頂端,變數查詢依賴該作用域鏈。

函式執行時會修改AO的值,會根據呼叫方式為this賦值,函式執行完畢後,上下文出棧(暫不考慮閉包)

閉包Closure

概念

記得7788的一句話:

A closure is a function plus the connection to the variables of its surrounding scopes.

閉包是由函式和函式建立時的詞法環境組成的,可以訪問到建立時能訪問到的所有區域性變數。

function a () {
    const c = 1
    return function b () {
        console.log(c)
    }
}
const d = a()
d()
複製程式碼

這裡存在一個閉包,函式b在建立環境之外呼叫時依然能夠訪問到建立時環境的區域性變數。

回到上一小節,閉包的情況下,因為內部函式存在對外部函式的變數引用,外部函式的執行上下文並不會出棧,導致記憶體佔用會增加,所以閉包其實並不是那麼美好。

注意

js內可以說是無處不閉包,很多時候我們只是沒有意識到這樣一個概念,其實很多地方已經都在用了

// 最簡單的情況,返回一個函式
function a () {
    return function b () {}
}
複製程式碼

題目來一個

var m = 1;
var obj = {
    m: 2,
    fn: function() {
        return function() {
            console.log(this.m)
        }
    }
}
obj.fn()() // 1
複製程式碼
var m = 1;
var obj = {
    m: 2,
    fn: function() {
        const that = this
        return function() {
            console.log(that.m)
        }
    }
}
obj.fn()() // 2
複製程式碼

雖發表於此,卻畢竟為一人之言,又是每日學有所得之筆記,內容未必詳實,看官老爺們還望海涵。

相關文章