Chain Pattern

The chain pattern is a way of calling methods in a chained manner. It does not belong to the category of the generally defined 23 design patterns, but is usually considered as a broadly categorized technique design pattern.

Description

Chained calls are very common in the JavaScript language, such as in jQuery, Promise, etc., all of which use chained calls. When we need to access the properties or methods of the same object multiple times, we often have to write the object multiple times for the . or () operations. Chained calls are a way to simplify this process, making the code concise and readable.
There are usually several ways to implement chained calls, but fundamentally they are similar, all by returning objects for later calls.

  • The scope chain of this, which is the implementation method of jQuery, is usually the method used for chained calls.
  • Returning the object itself, unlike this, explicitly returns the chained object.
  • Implementation through closure to return objects, which is similar to currying.
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}
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}
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

Optional Chaining Operator

When it comes to chained calls, it is necessary to mention the optional chaining operator in JavaScript, which is part of the new ES2020 features, including operators such as ?., ??, and ??=. The optional chaining operator ?. allows reading the value of a property deep within an object chain without having to explicitly validate each reference in the chain. The ?. operator functions similarly to the . chaining operator, but the difference is that it does not cause an error when the reference is nullish, i.e., null or undefined, and the expression short-circuits to return the value of undefined. When used with function calls, if the given function does not exist, it returns undefined. When attempting to access a property of an object that may not exist, the optional chaining operator makes the expression shorter and more concise. It is also very helpful when exploring the content of an object if it is uncertain which properties must exist.

Syntax

obj?.prop
obj?.[expr]
arr?.[index]
func?.(args)

Examples

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

Chained Calls in jQuery

jQuery is a high-end but luxurious framework with many wonderful methods and logic. Although it is not as popular now as MVVM-mode frameworks like Vue and React, the design of jQuery is great and definitely worth learning. Let's take the most basic instantiation of jQuery as an example here to explore how jQuery achieves chained calls using this.

First, let's define a basic class and use the prototype chain to inherit methods.

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

By defining a class and instantiating it, the instances can share methods on the prototype, but calling methods directly on the _jQuery class obviously won't work. The first type of exception thrown is because there are no static methods on the _jQuery class, and the second type of exception is because _jQuery as a function call didn't return a value. Here we can see that when calling jQuery via $(), it returns an object containing multiple methods, which we cannot access directly. We have to use another variable to access it.

function _jQuery(){
    return  _fn;
}
var _fn = _jQuery.prototype = {
    constructor: _jQuery,
    length: 2,
    size: function(){
        return this.length;
    }
}
console.log(_jQuery().size()); // 2

Actually, in order to reduce variable creation, jQuery directly treats _fn as a property of _jQuery.

function _jQuery(){
    return  _jQuery.fn;
}
_jQuery.fn = _jQuery.prototype = {
    constructor: _jQuery,
    length: 2,
    size: function(){
        return this.length;
    }
}
console.log(_jQuery().size()); // 2

So far, it can indeed achieve the invocation of methods on the prototype via _jQuery(), but in jQuery, the main purpose of $() is to be used as a selector to select elements, and what is currently returned is an _jQuery.fn object, which is obviously not what we want. In order to obtain the returned elements, we need to define an init method on the prototype to obtain the elements. Here, for simplicity, we directly use document.querySelector, while in reality, the construction of the jQuery selector is much more complex.

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")); // <body>...</body>

But it seems like this leaves out the chained call of this, so we need to use the pointing of this. Since this always points to the object that calls it, we just need to mount the selected element on the this object here.

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: ƒ}

But then a problem emerged. The elements selected by our selector are directly attached to _jQuery.fn. In this way, since the prototype is shared, subsequently defined selectors will overwrite the ones defined earlier. This obviously won't work. So, we use the new operator to create a new object.

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

This introduced another issue. When we use new to instantiate _jQuery.fn.init, the this returned points to the instance of _jQuery.fn.init, which prevents us from making chained calls. jQuery used a very clever method to solve this problem by directly pointing the prototype of _jQuery.fn.init to _jQuery.prototype. Although there might be a problem of circular reference, the performance overhead is not significant. Thus, we have completed the implementation of the jQuery selector and chained calls.

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

Daily Challenge

https://github.com/WindrunnerMax/EveryDay

Reference

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