1. 程式人生 > >JavaScript中的鏈式呼叫

JavaScript中的鏈式呼叫

# 鏈模式 鏈模式是一種鏈式呼叫的方式,準確來說不屬於通常定義的設計模式範疇,但鏈式呼叫是一種非常有用的程式碼構建技巧。 ## 描述 鏈式呼叫在`JavaScript`語言中很常見,如`jQuery`、`Promise`等,都是使用的鏈式呼叫,當我們在呼叫同一物件多次其屬性或方法的時候,我們需要多次書寫物件進行`.`或`()`操作,鏈式呼叫是一種簡化此過程的一種編碼方式,使程式碼簡潔、易讀。 鏈式呼叫通常有以下幾種實現方式,但是本質上相似,都是通過返回物件供之後進行呼叫。 * `this`的作用域鏈,`jQuery`的實現方式,通常鏈式呼叫都是採用這種方式。 * 返回物件本身, 同`this`的區別就是顯示返回鏈式物件。 * 閉包返回物件的方式實現,這種方式與柯里化有相似之處。 ```javascript var Person = function() {}; Person.prototype.setAge = function(age){ this.age = age; return this; } Person.prototype.setWeight = function(weight){ this.weight = weight; return this; } Person.prototype.get = function(){ return `{age: ${this.age}, weight: ${this.weight}}`; } var person = new Person(); var des = person.setAge(10).setWeight(30).get(); console.log(des); // {age: 10, weight: 30} ``` ```javascript var person = { age: null, weight: null, setAge: function(age){ this.age = age; return this; }, setWeight: function(weight){ this.weight = weight; return this; }, get: function(){ return `{age: ${this.age}, weight: ${this.weight}}`; } }; var des = person.setAge(10).setWeight(30).get(); console.log(des); // {age: 10, weight: 30} ``` ```javascript function numsChain(num){ var nums = num; function chain(num){ nums = `${nums} -> ${num}`; return chain; } chain.get = () => nums; return chain; } var des = numsChain(1)(2)(3).get(); console.log(des); // 1 -> 2 -> 3 ``` ## 可選鏈操作符 說到鏈式呼叫,就有必要說一下`JavaScript`的可選鏈操作符,屬於`ES2020`新特性運算子`?.`、`??`、`??=`,可選鏈操作符`?.`允許讀取位於連線物件鏈深處的屬性的值,而不必明確驗證鏈中的每個引用是否有效。`?.`操作符的功能類似於`.`鏈式操作符,不同之處在於在引用為空`nullish`即`null`或者`undefined`的情況下不會引起錯誤,該表示式短路返回值是`undefined`。與函式呼叫一起使用時,如果給定的函式不存在,則返回`undefined`。當嘗試訪問可能不存在的物件屬性時,可選鏈操作符將會使表示式更短更簡明。在探索一個物件的內容時,如果不能確定哪些屬性必定存在,可選鏈操作符也是很有幫助的。 ### 語法 ```javascript obj?.prop obj?.[expr] arr?.[index] func?.(args) ``` ### 示例 ```javascript const obj = {a: {}}; console.log(obj.a); // {} console.log(obj.a.b); // undefined // console.log(obj.a.b.c); // Uncaught TypeError: Cannot read property 'c' of undefined console.log(obj && obj.a); // {} console.log(obj && obj.a && obj.a.b && obj.a.b.c); // undefined console.log(obj?.a?.b?.c); // undefined const test = void 0; const prop = "a"; console.log(test); // undefined console.log(test?.a); // undefined console.log(test?.[prop]); // undefined console.log(test?.[0]); // undefined console.log(test?.()); // undefined ``` ## jQuery中的鏈式呼叫 `jQuery`是一個高階而不失奢華的框架,其中有許多非常精彩的方法和邏輯,雖然現在非常流行於類似於`Vue`、`React`的`MVVM`模式的框架,但是`jQuery`的設計實在是棒,非常值得學習,在這裡以最基礎的例項化`jQuery`為例探查一下`jQuery`如何通過`this`實現的鏈式呼叫。 首先定義一個最基本的類,通過原型鏈去繼承方法。 ```javascript function _jQuery(){} _jQuery.prototype = { constructor: _jQuery, length: 2, size: function(){ return this.length; } } var instance = new _jQuery(); console.log(instance.size()); // 2 // _jQuery.size() // Uncaught TypeError: _jQuery.size is not a function // _jQuery().size() / /Uncaught TypeError: Cannot read property 'size' of undefined ``` 通過定義一個類並且實現例項化之後,在例項之間可以共享原型上的方法,而直接通過`_jQuery`類直接去呼叫顯然是不行的,丟擲的第一種異常是因為在`_jQuery`類上不存在靜態方法,第二種異常是因為`_jQuery`作為函式執行後未返回值,通過這裡可以看出`jQuery`在通過`$()`方式呼叫的時候是返回了一個包含多個方法的物件的,而只是通過自己是訪問不到的,我們就藉助另一個變數去訪問。 ```javascript function _jQuery(){ return _fn; } var _fn = _jQuery.prototype = { constructor: _jQuery, length: 2, size: function(){ return this.length; } } console.log(_jQuery().size()); // 2 ``` 實際上`jQuery`為了減少變數的建立,直接將`_fn`看做了`_jQuery`的一個屬性。 ```javascript function _jQuery(){ return _jQuery.fn; } _jQuery.fn = _jQuery.prototype = { constructor: _jQuery, length: 2, size: function(){ return this.length; } } console.log(_jQuery().size()); // 2 ``` 到這裡確實能夠實現`_jQuery()`方式呼叫原型上的方法,但是在`jQuery`中`$()`的主要目標還是作為選擇器用來選擇元素,而現在返回的是一個`_jQuery.fn`物件,顯然是達不到要求的,為了能夠取得返回的元素,那就在原型上定義一個`init`方法去獲取元素,這裡為了省事直接使用了`document.querySelector`,實際上`jQuery`的選擇器構建是很複雜的。 ```javascript function _jQuery(selector){ return _jQuery.fn.init(selector); } _jQuery.fn = _jQuery.prototype = { constructor: _jQuery, init: function(selector){ return document.querySelector(selector); }, length: 3, size: function(){ return this.length; } } console.log(_jQuery("body")); // ...
``` 但是似乎這樣又把鏈式呼叫的`this`給漏掉了,這裡就需要利用`this`的指向了,因為在呼叫時`this`總是指向呼叫他的物件,所以我們在這裡將選擇的元素掛載到`this`物件上即可。 ```javascript function _jQuery(selector){ return _jQuery.fn.init(selector); } _jQuery.fn = _jQuery.prototype = { constructor: _jQuery, init: function(selector){ this[0] = document.querySelector(selector); this.length = 1; return this; }, length: 3, size: function(){ return this.length; } } var body = _jQuery("body"); console.log(body); // {0: body, length: 1, constructor: ƒ, init: ƒ, size: ƒ} console.log(body.size()); // 1 console.log(_jQuery.fn); // {0: body, length: 1, constructor: ƒ, init: ƒ, size: ƒ} ``` 但是此時又出現了一個問題,我們的選擇器選擇的元素是直接掛載到了`_jQuery.fn`上,這樣的話由於原型是共享的,在之後的定義的選擇器就會將前邊定義的選擇器覆蓋掉,這樣顯然是不行的,於是我們使用`new`操作符新建一個物件。 ```javascript function _jQuery(selector){ return new _jQuery.fn.init(selector); } _jQuery.fn = _jQuery.prototype = { constructor: _jQuery, init: function(selector){ this[0] = document.querySelector(selector); this.length = 1; return this; }, length: 3, size: function(){ return this.length; } } var body = _jQuery("body"); console.log(body); // init {0: body, length: 1} // console.log(body.size()); // Uncaught TypeError: body.size is not a function ``` 這樣又出現了問題,當我們使用`new`例項化`_jQuery.fn.init`時返回的`this`指向的是`_jQuery.fn.init`的例項,我們就不能進行鏈式呼叫了,`jQuery`用了一個非常巧妙的方法解決了這個問題,直接將`_jQuery.fn.init`的原型指向`_jQuery.prototype`,雖然會有迴圈引用的問題,但是相對來說這一點效能消耗並不算什麼,由此我們完成了`jQuery`選擇器以及鏈式呼叫的實現。 ```javascript function _jQuery(selector){ return new _jQuery.fn.init(selector); } _jQuery.fn = _jQuery.prototype = { constructor: _jQuery, init: function(selector){ this[0] = document.querySelector(selector); this.length = 1; return this; }, length: 3, size: function(){ return this.length; } } _jQuery.fn.init.prototype = _jQuery.fn; var body = _jQuery("body"); console.log(body); // init {0: body, length: 1} console.log(body.size()); // 1 console.log(_jQuery.fn.init.prototype.init.prototype.init.prototype === _jQuery.fn); // true ``` ## 每日一題 ``` https://github.com/WindrunnerMax/EveryDay ``` ## 參考 ``` https://zhuanlan.zhihu.com/p/110512501 https://juejin.cn/post/6844904030221631495 https://segmentfault.com/a/1190000011863232 https://github.com/songjinzhong/JQuerySource https://leohxj.gitbooks.io/front-end-database/content/jQuery/jQuery-source-code/index.html https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/%E5%8F%AF%E9%80%89%E9%93%BE ```