您好,登錄后才能下訂單哦!
小編給大家分享一下JavaScript的詳細分析示例,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
在講解它之前我們首先需要澄清一個非常常見的關于 JavaScript 中函數和對象的誤解:
在傳統的面向類的語言中,“構造函數”是類中的一些特殊方法,使用 new
初始化類時會調用類中的構造函數。通常的形式是這樣的:
something = new MyClass(..);
JavaScript 也有一個 new
操作符,使用方法看起來也和那些面向類的語言一樣,絕大多數開發者都認為 JavaScript 中 new
的機制也和那些語言一樣。然而,JavaScript 中 new
的機制實際上和面向類的語言完全不同。
首先我們重新定義一下 JavaScript 中的“構造函數”。在 JavaScript 中,構造函數只是一些使用 new
操作符時被調用的函數。它們并不會屬于某個類,也不會實例化一個類。實際上,它們甚至都不能說是一種特殊的函數類型,它們只是被 new
操作符調用的普通函數而已。
實際上并不存在所謂的“構造函數”,只有對于函數的“構造調用”。
使用 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()
方法搞清楚它是如何做到的。
在相當長的一段時間里,JavaScript 只有一些近似類的語法元素,如new
和 instanceof
,不過在后來的 ES6 中新增了一些元素,比如 class
關鍵字。
在不考慮class
的前提下,new
和instanceof
之間的關系“曖昧不清”。之所以會出現new
和instanceof
這些操作符,其主要目的就是為了向“面向對象編程”靠攏。
因此,我們既然搞懂了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()
方法創建一個新對象,使用現有的對象來提供新創建的對象的__proto__
。
在《你不知道的JavaScript》中,多次用到了Object.create()
這個方法去模仿傳統面向對象編程中的“繼承”,其中也包括上面講到了new
操作符的實現過程。在MDN中對它的介紹也很簡短,主要內容大都在描述可選參數propertiesObject
的用法。
簡單起見,為了和new
、instanceof
的知識串聯起來,我們只著重關注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__
屬性的方式,因為每個瀏覽器的實現不盡相同,而且在規范中也沒有明確該屬性名)。
作為最經典的手寫“勞模”們,call
、apply
和bind
已經被手寫了無數遍。也許本文中手寫的版本是無數個前輩們寫過的某個版本,但是有一點不同的是,本文會告訴你為什么要這樣寫,讓你搞懂了再寫。
在《你不知道的JavaScript上卷》第二部分的第1章和第2章,用了2章斤30頁的篇幅中詳細地介紹了this
的內容,已經充分說明了this
的重要性和應用場景的復雜性。
而我們要實現的call
、apply
和bind
最為人所知的功能就是使用指定的thisArg
去調用函數,使得函數可以使用我們指定的thisArg
作為它運行時的上下文。
《你不知道的JavaScript》總結了四條規則來判斷一個運行中函數的this
到底是綁定到哪里:
new
調用?綁定到新創建的對象。call
或者 apply
(或者 bind
)調用?綁定到指定的對象。undefined
,否則綁定到全局對象。更具體一點,可以描述為:
new
中調用( new
綁定)?如果是的話 this
綁定的是新創建的對象:var bar = new foo()
call
、 apply
(顯式綁定)或者硬綁定(bind
)調用?如果是的話, this
綁定的是指定的對象:var bar = foo.call(obj2)復制代碼
this
綁定的是那個上下文對象:var bar = obj1.foo()復制代碼
undefined
,否則綁定到全局對象:var bar = foo()復制代碼
就是這樣。對于正常的函數調用來說,理解了這些知識你就可以明白 this
的綁定原理了。
至此,你已經搞明白了this
的全部綁定規則,而我們要去手寫實現的call
、apply
和bind
只是其中的一條規則(第2條),因此,我們可以在另外3條規則的基礎上很容易地組織代碼實現。
實現call
和apply
的通常做法是使用“隱式綁定”的規則,只需要綁定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)
作為鍵名是為了避免和上下文對象的其他鍵名沖突,從而導致覆蓋了原有的屬性鍵值對。
在《你不知道的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
函數的x
,y
兩個參數變成了先用一個函數接收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的詳細分析示例的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。