91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

JavaScript的詳細分析示例

發布時間:2020-11-18 10:39:00 來源:億速云 閱讀:152 作者:小新 欄目:web開發

小編給大家分享一下JavaScript的詳細分析示例,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

一、操作符new

在講解它之前我們首先需要澄清一個非常常見的關于 JavaScript 中函數和對象的誤解:

在傳統的面向類的語言中,“構造函數”是類中的一些特殊方法,使用 new 初始化類時會調用類中的構造函數。通常的形式是這樣的:

something = new MyClass(..);

JavaScript 也有一個 new 操作符,使用方法看起來也和那些面向類的語言一樣,絕大多數開發者都認為 JavaScript 中 new 的機制也和那些語言一樣。然而,JavaScript 中 new 的機制實際上和面向類的語言完全不同。

首先我們重新定義一下 JavaScript 中的“構造函數”。在 JavaScript 中,構造函數只是一些使用 new 操作符時被調用的函數。它們并不會屬于某個類,也不會實例化一個類。實際上,它們甚至都不能說是一種特殊的函數類型,它們只是被 new 操作符調用的普通函數而已。

實際上并不存在所謂的“構造函數”,只有對于函數的“構造調用”。 使用 new 來調用函數,或者說發生構造函數調用時,會自動執行下面的操作:

  1. 創建(或者說構造)一個全新的對象;
  2. 這個新對象會被執行 [[ 原型 ]] 連接;
  3. 這個新對象會綁定到函數調用的 this ;
  4. 如果函數沒有返回其他對象,那么 new 表達式中的函數調用會自動返回這個新對象。

因此,如果我們要想寫出一個合乎理論的 new ,就必須嚴格按照上面的步驟,落實到代碼上就是:

/**
* @param {fn} Function(any) 構造函數
* @param {arg1, arg2, ...} 指定的參數列表
*/
function myNew (fn, ...args) {
    // 創建一個新對象,并把它的原型鏈(__proto__)指向構造函數的原型對象
    const instance = Object.create(fn.prototype)

    // 把新對象作為thisArgs和參數列表一起使用call或apply調用構造函數
    const result = fn.apply(instance, args)

    如果構造函數的執行結果返回了對象類型的數據(排除null),則返回該對象,否則返新對象
    return (result && typeof instance === 'object') ? result : instance
}  

示例代碼中,我們使用Object.create(fn.prototype)創建空對象,使其的原型鏈__proto__指向構造函數的原型對象fn.prototype,后面我們也會自己手寫一個Object.create()方法搞清楚它是如何做到的。

二、操作符instanceof

在相當長的一段時間里,JavaScript 只有一些近似類的語法元素,如newinstanceof,不過在后來的 ES6 中新增了一些元素,比如 class 關鍵字。

在不考慮class的前提下,newinstanceof之間的關系“曖昧不清”。之所以會出現newinstanceof這些操作符,其主要目的就是為了向“面向對象編程”靠攏。

因此,我們既然搞懂了new,就沒有理由不去搞清楚instanceof。引用MDN上對于instanceof的描述:“instanceof 運算符用于檢測構造函數的 prototype 屬性是否出現在某個實例對象的原型鏈上”。

看到這里,基本上明白了,instanceof的實現需要考驗你對原型鏈和prototype的理解。在JavaScript中關于原型和原型鏈的內容需要大篇幅的內容才能講述得清楚,而網上也有一些不錯的總結博文,其中幫你徹底搞懂JS中的prototype、__proto__與constructor(圖解)就是一篇難得的精品文章,通透得梳理并總結了它們之間的關系和聯系。

《你不知道的JavaScript上卷》第二部分-第5章則更基礎、更全面地得介紹了原型相關的內容,值得一讀。

以下instanceof代碼的實現,雖然很簡單,但是需要建立在你對原型和原型鏈有所了解的基礎之上,建議你先把以上的博文或文章看懂了再繼續。

/**
* @param {left} Object 實例對象
* @param {right} Function 構造函數
*/
function myInstanceof (left, right) {
    // 保證運算符右側是一個構造函數
    if (typeof right !== 'function') {
        throw new Error('運算符右側必須是一個構造函數')
        return
    }

    // 如果運算符左側是一個null或者基本數據類型的值,直接返回false 
    if (left === null || !['function', 'object'].includes(typeof left)) {
        return false
    }

    // 只要該構造函數的原型對象出現在實例對象的原型鏈上,則返回true,否則返回false
    let proto = Object.getPrototypeOf(left)
    while (true) {

        // 遍歷完了目標對象的原型鏈都沒找到那就是沒有,即到了Object.prototype

        if (proto === null) return false

        // 找到了
        if (proto === right.prototype) return true

        // 沿著原型鏈繼續向上找
        proto = Object.getPrototypeOf(proto)
    }
}

三、 Object.create()

Object.create()方法創建一個新對象,使用現有的對象來提供新創建的對象的__proto__

在《你不知道的JavaScript》中,多次用到了Object.create()這個方法去模仿傳統面向對象編程中的“繼承”,其中也包括上面講到了new操作符的實現過程。在MDN中對它的介紹也很簡短,主要內容大都在描述可選參數propertiesObject的用法。

簡單起見,為了和newinstanceof的知識串聯起來,我們只著重關注Object.create()的第一個參數proto,咱不討論propertiesObject的實現和具體特性。

/**
* 基礎版本
* @param {Object} proto
*  
*/
Object.prototype.create = function (proto) {  
    // 利用new操作符的特性:創建一個對象,其原型鏈(__proto__)指向構造函數的原型對象
    function F () {}
    F.prototype = proto
    return new F()
}

/**
* 改良版本
* @param {Object} proto
*
*/
Object.prototype.createX = function (proto) {
    const obj = {}
    // 一步到位,把一個空對象的原型鏈(__proto__)指向指定原型對象即可
    Object.setPrototypeOf(obj, proto)
    return obj
}

我們可以看到,Object.create(proto)做的事情轉換成其他方法實現很簡單,就是創建一個空對象,再把這個對象的原型鏈屬性(Object.setPrototype(obj, proto))指向指定的原型對象proto就可以了(不要采用直接賦值__proto__屬性的方式,因為每個瀏覽器的實現不盡相同,而且在規范中也沒有明確該屬性名)。

四、Function的原型方法:call、apply和bind

作為最經典的手寫“勞模”們,callapplybind已經被手寫了無數遍。也許本文中手寫的版本是無數個前輩們寫過的某個版本,但是有一點不同的是,本文會告訴你為什么要這樣寫,讓你搞懂了再寫。

在《你不知道的JavaScript上卷》第二部分的第1章和第2章,用了2章斤30頁的篇幅中詳細地介紹了this的內容,已經充分說明了this的重要性和應用場景的復雜性。

而我們要實現的callapplybind最為人所知的功能就是使用指定的thisArg去調用函數,使得函數可以使用我們指定的thisArg作為它運行時的上下文。

《你不知道的JavaScript》總結了四條規則來判斷一個運行中函數的this到底是綁定到哪里:

  1. new 調用?綁定到新創建的對象。
  2. call 或者 apply (或者 bind )調用?綁定到指定的對象。
  3. 由上下文對象調用?綁定到那個上下文對象。
  4. 默認:在嚴格模式下綁定到 undefined ,否則綁定到全局對象。

更具體一點,可以描述為:

  1. 函數是否在 new 中調用( new 綁定)?如果是的話 this 綁定的是新創建的對象:
var bar = new foo()
  1. 函數是否通過 callapply (顯式綁定)或者硬綁定(bind)調用?如果是的話, this 綁定的是指定的對象:
var bar = foo.call(obj2)復制代碼
  1. 函數是否在某個上下文對象中調用(隱式綁定)?如果是的話, this 綁定的是那個上下文對象:
var bar = obj1.foo()復制代碼
  1. 如果都不是的話,使用默認綁定。如果在嚴格模式下,就綁定到 undefined ,否則綁定到全局對象:
var bar = foo()復制代碼

就是這樣。對于正常的函數調用來說,理解了這些知識你就可以明白 this 的綁定原理了。

至此,你已經搞明白了this的全部綁定規則,而我們要去手寫實現的callapplybind只是其中的一條規則(第2條),因此,我們可以在另外3條規則的基礎上很容易地組織代碼實現。

4.1 call和apply

實現callapply的通常做法是使用“隱式綁定”的規則,只需要綁定thisArg對象到指定的對象就好了,即:使得函數可以在指定的上下文對象中調用:

const context = {
    name: 'ZhangSan'
}
function sayName () {
    console.log(this.name)
}
context.sayName = sayName
context.sayName() // ZhangSan

這樣,我們就完成了“隱式綁定”。落實到具體的代碼實現上:

/**
* @param {context} Object 
* @param {arg1, arg2, ...} 指定的參數列表
*/
Function.prototype.call = function (context, ...args) {
    // 指定為 null 或 undefined 時會自動替換為指向全局對象,原始值會被包裝
    if (context === null || context === undefined) {
        context = window
    } else if (typeof context !== 'object') {
        context = new context.constructor(context)
    } else {
        context = context
    }
    const func = this
    const fn = Symbol('fn')
    context[fn] = func
    const result = context[fn](...args)
    delete context[fn]
    return result
}

/**
* @param {context}
* @param {args} Array 參數數組
*/
Function.prototype.apply = function (context, args) {
    // 和call一樣的原理
    if (context === null || context === undefined) {
        context = window
    } else if (typeof context !== 'object') {
        context = new context.constructor(context)
    } else {
        context = context
    }
    const fn = Symbol('fn')
    const func = this
    context[fn] = func
    const result = context[fn](...args)
    delete context[fn]
    return result
}

細看下來,大家都那么聰明,肯定一眼就看到了它們的精髓所在:

const fn = Symbol('fn')
const func = this
context[fn] = func

在這里,我們使用Symbol('fn')作為上下文對象的鍵,對應的值指向我們想要綁定上下文的函數this(因為我們的方法是聲明在Function.prototype上的),而使用Symbol(fn)作為鍵名是為了避免和上下文對象的其他鍵名沖突,從而導致覆蓋了原有的屬性鍵值對。

4.2 bind

在《你不知道的JavaScript》中,手動實現了一個簡單版本的bind函數,它稱之為“硬綁定”:

function bind(fn, obj) {
    return function() {
        return fn.apply( obj, arguments );
    };
}

硬綁定的典型應用場景就是創建一個包裹函數,傳入所有的參數并返回接收到的所有值。

由于硬綁定是一種非常常用的模式,所以在 ES5 中提供了內置的方法 Function.prototype.bind ,它的用法如下:

function foo(something) {
    console.log( this.a, something )
    return this.a + something;
}

var obj = {
    a:2
}

var bar = foo.bind( obj )

var b = bar( 3 ); // 2 3

console.log( b ); // 5

bind(..) 會返回一個硬編碼的新函數,它會把參數設置為 this 的上下文并調用原始函數。

MDN是這樣描述bind方法的:bind() 方法創建一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定為 bind() 的第一個參數,而其余參數將作為新函數的參數,供調用時使用。

因此,我們可以在此基礎上實現我們的bind方法:

/**
* @param {context} Object 指定為 null 或 undefined 時會自動替換為指向全局對象,原始值會被包裝
*
* @param {arg1, arg2, ...} 指定的參數列表
*
* 如果 bind 函數的參數列表為空,或者thisArg是null或undefined,執行作用域的 this 將被視為新函數的 thisArg
*/
Function.prototype.bind = function (context, ...args) {
    if (typeof this !== 'function') {
        throw new TypeError('必須使用函數調用此方法');
    }
    const _self = this

    // fNOP存在的意義:
    //  1. 判斷返回的fBound是否被new調用了,如果是被new調用了,那么fNOP.prototype自然是fBound()中this的原型
    //  2. 使用包裝函數(_self)的原型對象覆蓋自身的原型對象,然后使用new操作符構造出一個實例對象作為fBound的原型對象,從而實現繼承包裝函數的原型對象
    const fNOP = function () {}

    const fBound = function (...args2) {

        // fNOP.prototype.isPrototypeOf(this) 為true說明當前結果是被使用new操作符調用的,則忽略context
        return _self.apply(fNOP.prototype.isPrototypeOf(this) && context ? this : context, [...args, ...args2])
    }

    // 綁定原型對象
    fNOP.prototype = this.prototype
    fBound.prototype = new fNOP()
    return fBound
}

具體的實現細節都標注了對應的注釋,涉及到的原理都有在上面的內容中講到,也算是一個總結和回顧吧。

五、函數柯里化

維基百科:柯里化,英語:Currying,是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,并且返回接受余下的參數而且返回結果的新函數的技術 。

看這個解釋有一點抽象,我們就拿被做了無數次示例的add函數,來做一個簡單的實現:

// 普通的add函數
function add(x, y) {
    return x + y
}

// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3

實際上就是把add函數的xy兩個參數變成了先用一個函數接收x然后返回一個函數去處理y參數。現在思路應該就比較清晰了,就是只傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。

函數柯里化在一定場景下,有很多好處,如:參數復用、提前確認和延遲運行等,具體內容可以拜讀下這篇文章,個人覺得受益匪淺。

最簡單的實現函數柯里化的方式就是使用Function.prototype.bind,即:

function curry(fn, ...args) {
    return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}

如果用ES5代碼實現的話,會比較麻煩些,但是核心思想是不變的,就是在傳遞的參數滿足調用函數之前始終返回一個需要傳參剩余參數的函數:

// 函數柯里化指的是一種將使用多個參數的一個函數轉換成一系列使用一個參數的函數的技術。
function curry(fn, args) {
    args = args || []

    // 獲取函數需要的參數長度
    let length = fn.length
    return function() {
        let subArgs = args.slice(0)

        // 拼接得到現有的所有參數
        for (let i = 0; i < arguments.length; i++) {
        subArgs.push(arguments[i])
        }

        // 判斷參數的長度是否已經滿足函數所需參數的長度
        if (subArgs.length >= length) {
            // 如果滿足,執行函數
            return fn.apply(this, subArgs)
        } else {
            // 如果不滿足,遞歸返回科里化的函數,等待參數的傳入
            return curry.call(this, fn, subArgs)
        }
    };
}

以上是JavaScript的詳細分析示例的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

盐山县| 克什克腾旗| 广东省| 寿阳县| 栾川县| 南通市| 临夏市| 岳池县| 岱山县| 长宁县| 台安县| 南丹县| 富平县| 垣曲县| 依兰县| 滨州市| 三原县| 伊宁市| 布尔津县| 宣化县| 体育| 阿克陶县| 洛浦县| 乌鲁木齐县| 昔阳县| 时尚| 江孜县| 宾阳县| 南召县| 和顺县| 凌云县| 长春市| 荃湾区| 襄城县| 嘉义市| 邹平县| 嘉黎县| 如东县| 枣庄市| 新和县| 宜兰市|