JavaScript设计模式(二十六):永无尽头-链模式

简介: 永无尽头-链模式

链模式(Operate of Responsibility)

通过在对象方法中将当前对象返回,实现对同一个对象多个方法的链式调用。从而简化对该对象的多个方法的多次调用时,对该对象的多次引用。

深究jQuery(参考版本: 2.2.4版本)

jQuery的点语法是基于原型继承实现的,并且在每一个原型方法的实现上都返回当前对象this,使当前对象一直处于原型链作用域的顶端。

实现类似于jQuery的框架语法

<p id="text"></p>
// 实现目标如下:
let text = $('#text');

console.log(text); // jQuery.fn.init [p#text, context: document, selector: '#text']

console.log(text.size()); // 1

text
    .css({
   
   
        padding: '10px',
        'line-height': '30px',
        'background-color': 'red'
    })
    .attr('class', 'user-name')
    .html('ProsperLee')
    .on('click', function () {
   
   
        console.log('clicked');
    });

console.log(text); // jQuery.fn.init [p#text.user-name, context: document, selector: '#text']

原型式继承

let A = function () {
   
   
};

A.prototype = {
   
   
    length: 0,
    size: function () {
   
   
        return this.length;
    }
};

let a = new A();

console.log(a);             // A{}
console.log(a.__proto__);   // {length: 0, size: ƒ}

a.size();                   // 0
A.prototype.size();         // 0

A();        // undefined
A().size(); // 【Error】 Uncaught TypeError: Cannot read properties of undefined (reading 'size')
A.size();   // 【Error】 Uncaught TypeError: A.size is not a function

找位助手

此时因为A()返回的是undefined,所以使用A().size();访问不到.于是我们可以借助第三方对象进行访问

let A = function () {
   
   
    return B;
};

let B = A.prototype = {
   
   
    length: 0,
    size: function () {
   
   
        return this.length;
    }
};

A().size(); // 0

而在jQuery中,为了减少变量的定义,将想使用的变量作为了A的一个属性

let A = function () {
   
   
    return A.fn;
};

A.fn = A.prototype = {
   
   
    length: 0,
    size: function () {
   
   
        return this.length;
    }
};

A().size(); // 0

获取元素

jQuery的目的是为了获取元素,返回的是一组元素簇(元素的聚合对象),但现在返回的却是一个A.fn对象,显然达不到我们的需求;所以,如果A.fn能提供给我们一个获取元素的方法init就好了.

let A = function (selector) {
   
   
    return A.fn.init(selector);
};

A.fn = A.prototype = {
   
   
    init: function (selector) {
   
   
        return document.getElementById(selector);
    },
    length: 0,
    size: function () {
   
   
        return this.length;
    }
};

A('container'); // <div id="container"></div>
A('container').size(); // Error ---> Uncaught TypeError: A(...).size is not a function

两个问题

此时是无法访问size方法的,因为init返回的是DOM元素,故可以使用this解决

let A = function (selector) {
   
   
    return A.fn.init(selector);
};

A.fn = A.prototype = {
   
   
    init: function (selector) {
   
   
        this.selector = selector;
        this[0] = document.getElementById(selector);
        this.length = 1; // 重置length
        return this;
    },
    length: 0,
    size: function () {
   
   
        return this.length;
    }
};

A('container'); // {0: div#container, length: 1, selector: 'container', init: ƒ, size: ƒ}
A('container').size(); // 1

覆盖问题: box2覆盖了box1

let box1 = A('container1');
let box2 = A('container2');

console.log(box1); // {0: div#container2, length: 1, selector: 'container2', init: ƒ, size: ƒ}
console.log(box2); // {0: div#container2, length: 1, selector: 'container2', init: ƒ, size: ƒ}

覆盖获取

后获取id的DOM元素会将先获取id的DOM元素进行覆盖,是因为每次在A的构造函数中返回的A.fn.init(selector);对象指向同一个对象造成的

解决方法: 使用new关键字进行处理,让被赋值的变量都是独立的对象

let A = function (selector) {
   
   
    return new A.fn.init(selector);
};

A.fn = A.prototype = {
   
   
    init: function (selector) {
   
   
        this.selector = selector;
        this[0] = document.getElementById(selector);
        this.length = 1; // 重置length
        return this;
    },
    length: 0,
    size: function () {
   
   
        return this.length;
    }
};

let box1 = A('container1');
let box2 = A('container2');

console.log(box1); // {0: div#container1, length: 1, selector: 'container1', init: ƒ, size: ƒ}
console.log(box2); // {0: div#container2, length: 1, selector: 'container2', init: ƒ, size: ƒ}

box1.length; // 1
box1.size(); // Error ---> Uncaught TypeError: box1.size is not a function

方法丢失

因为我们使用new关键字,导致无法在init原型上找到size方法,导致方法丢失,所以访问size方法报错

let A = function (selector) {
   
   
    return new A.fn.init(selector);
};

A.fn = A.prototype = {
   
   
    init: function (selector) {
   
   
        console.log(this === A.fn);
        console.log(this === A.prototype);
        this.selector = selector;
        this[0] = document.getElementById(selector);
        this.length = 1; // 重置length
        return this;
    },
    length: 0,
    size: function () {
   
   
        return this.length;
    }
};

// 未使用new的
A.fn.init('container');     // true true

// 使用了new的,所以找不到size方法,解决方案请向下看
A('container');             // false false

对比jQuery

对比返回对象的构造器constructor

let A = function (selector) {
   
   
    return new A.fn.init(selector);
};

A.fn = A.prototype = {
   
   
    init: function (selector) {
   
   
        this.selector = selector;
        this[0] = document.getElementById(selector);
        this.length = 1; // 重置length
        return this;
    },
    length: 0,
    size: function () {
   
   
        return this.length;
    }
};

console.log($('#container').constructor.name);      // jQuery
console.log(A('container').constructor.name);       // init

解决方法: 故需要我们去强化构造器

let A = function (selector) {
   
   
    return new A.fn.init(selector);
};

A.fn = A.prototype = {
   
   
    constructor: A, // 强化构造函数,使构造器constructor返回A对象
    init: function (selector) {
   
   
        this.selector = selector;
        this[0] = document.getElementById(selector);
        this.length = 1; // 重置length
        return this;
    },
    length: 0,
    size: function () {
   
   
        return this.length;
    }
};

// 要访问size方法需要添加这一行
A.fn.init.prototype = A.fn; // 单独添加这一行会导致constructor返回的是Object

console.log($('#container').constructor.name);          // jQuery
console.log(A('container').constructor.name);       // A

A('container').size(); // 1

丰富元素获取

丰富功能,可以获取各种选择器返回的DOM元素


<div id="box0">
    <ul id="box1">
        <li class="item"></li>
        <li></li>
        <li class="item"></li>
    </ul>
    <div class="item"></div>
</div>
let A = function (selector, context) {
   
   
    return new A.fn.init(selector, context);
};

A.fn = A.prototype = {
   
   
    constructor: A,
    /**
     * 初始化
     * @param selector 选择器
     * @param context 上下文
     * @returns {A} 返回A对象
     */
    init: function (selector, context) {
   
   
        this.selector = selector;
        this.context = context || document;
        let doms = this.context.querySelectorAll(this.selector); // 获取元素集合
        this.length = doms.length;
        doms.forEach((dom, index) => this[index] = dom);
        return this;
    },
    length: 0,
    size: function () {
   
   
        return this.length;
    }
};

A.fn.init.prototype = A.fn;
/**
 * init {
 *      0: div#box0,
 *      selector: '#box0',
 *      context: document,
 *      length: 1
 * }
 */
console.log(A('#box0'));
/**
 * init {
 *      0: li.item,
 *      1: li.item,
 *      2: div.item,
 *      selector: '.item',
 *      context: document,
 *      length: 3
 * }
 */
console.log(A('.item'));
/**
 * init {
 *      0: li.item,
 *      1: li,
 *      2: li.item,
 *      selector: 'li',
 *      context: document,
 *      length: 3
 * }
 */
console.log(A('li'));
/**
 * init {
 *      0: li.item,
 *      1: li.item,
 *      selector: '.item',
 *      context: ul#box1,
 *      length: 2
 * }
 */
console.log(A('.item', document.getElementById('box1')));
/**
 * init {
 *      selector: 'xxx', 
 *      context: document, 
 *      length: 0
 * }
 */
console.log(A('xxx'));

数组与对象

由于JavaScript的弱类型语言,并且数组、对象、函数都被看成是对象的实例,所以JavaScript中并没有一个纯粹的数组类型,而且JavaScript引擎的实现也没有做严格的校验,也是基于对象实现的.

一些浏览器解析引擎在判断对象是否是数组的时候不仅仅判断其有没有length属性,可否通过[索引值]方式访问元素,还会判断其是否具有数组方法来确定是否要用数组的形式展现,所以我们只需要在A.fn中添加几个数组常用的方法来增强数组特性就可以解决问题了

let A = function (selector, context) {
   
   
    return new A.fn.init(selector, context);
};

A.fn = A.prototype = {
   
   
    constructor: A,

    // 增强数组(参见访问者模式)
    push: [].push,
    sort: [].sort,
    splice: [].splice,

    /**
     * 初始化
     * @param selector 选择器
     * @param context 上下文
     * @returns {A} 返回A对象
     */
    init: function (selector, context) {
   
   
        this.selector = selector;
        this.context = context || document;
        let doms = this.context.querySelectorAll(this.selector); // 获取元素集合
        this.length = doms.length;
        doms.forEach((dom, index) => this[index] = dom);
        return this;
    },
    length: 0,
    size: function () {
   
   
        return this.length;
    }
};

A.fn.init.prototype = A.fn;

console.log(A('#text')); // init [p#text.user-name, selector: '#text', context: document] ---> length: 1

方法拓展

对内部对象的拓展和对外部对象的拓展

let A = function (selector, context) {
   
   
    return new A.fn.init(selector, context);
};

A.fn = A.prototype = {
   
   
    constructor: A,

    // 增强数组
    push: [].push,
    sort: [].sort,
    splice: [].splice,

    /**
     * 初始化
     * @param selector 选择器
     * @param context 上下文
     * @returns {A} 返回A对象
     */
    init: function (selector, context) {
   
   
        this.selector = selector;
        this.context = context || document;
        let doms = this.context.querySelectorAll(this.selector); // 获取元素集合
        this.length = doms.length;
        doms.forEach((dom, index) => this[index] = dom);
        return this;
    },
    length: 0,
    size: function () {
   
   
        return this.length;
    }
};

A.fn.init.prototype = A.fn;

/**
 * 对象拓展 - 对内部对象的拓展和对外部对象的拓展
 * @type {A.extend}
 */
A.extend = A.fn.extend = function (...args) {
   
   
    // 如果只存在一个参数,那么是对内部对象进行拓展;
    // 如果是多个参数,那么第一个参数作为需要拓展的对象,为外部对象的拓展;
    let target = args.length <= 1 ? this : args[0];
    args.forEach(arg => {
   
   
        for (const k in arg) {
   
   
            target[k] = arg[k];
        }
    });
    return target;
};
A.extend({
   
   name: 'Tom'}, {
   
   age: 18}); // {name: 'Tom', age: 18}
A.extend(A.fn, {
   
   
    version: '1.0.0',
    getVersion: function () {
   
   
        return this.version;
    }
});
console.log(A('container').getVersion()); // 1.0.0
A.extend({
   
   
    version: 'v1.0.0',
    getVersion: function () {
   
   
        return this.version;
    }
});
console.log(A.getVersion()); // v1.0.0

添加方法并使用


<button class="btn">按钮01</button>
<button class="btn">按钮02</button>
<button class="btn">按钮03</button>
let A = function (selector, context) {
   
   
    return new A.fn.init(selector, context);
};

A.fn = A.prototype = {
   
   
    constructor: A,
    // 增强数组
    push: [].push,
    sort: [].sort,
    splice: [].splice,
    /**
     * 初始化
     * @param selector 选择器
     * @param context 上下文
     * @returns {A} 返回A对象
     */
    init: function (selector, context) {
   
   
        this.selector = selector;
        this.context = context || document;
        let doms = this.context.querySelectorAll(this.selector); // 获取元素集合
        this.length = doms.length;
        doms.forEach((dom, index) => this[index] = dom);
        return this;
    },
    length: 0,
    size: function () {
   
   
        return this.length;
    }
};

A.fn.init.prototype = A.fn;
/**
 * 对象拓展 - 对内部对象的拓展和对外部对象的拓展
 * @type {A.extend}
 */
A.extend = A.fn.extend = function (...args) {
   
   
    // 如果只存在一个参数,那么是对内部对象进行拓展;
    // 如果是多个参数,那么第一个参数作为需要拓展的对象,为外部对象的拓展;
    let target = args.length <= 1 ? this : args[0];
    args.forEach(arg => {
   
   
        for (const k in arg) {
   
   
            target[k] = arg[k];
        }
    });
    return target;
};
A.extend({
   
   
    // 将'-'分割线转化为驼峰式,如:'border-color' -> 'borderColor'
    camelCase: function (str) {
   
   
        return str.replace(/-(\w)/g, function (all, letter) {
   
   
            return letter.toUpperCase();
        });
    },

});

A.fn.extend({
   
   
    css: function (obj) {
   
   
        const len = this.length;
        for (let i = 0; i < len; i++) {
   
   
            for (const k in obj) {
   
   
                this[i].style[A.camelCase(k)] = obj[k];
            }
        }
        return this;
    },
    attr: function (key, value) {
   
   
        const len = this.length;
        for (let i = 0; i < len; i++) {
   
   
            this[i].setAttribute(key, value);
        }
        return this;
    },
    html: function (htmlStr) {
   
   
        const len = this.length;
        for (let i = 0; i < len; i++) {
   
   
            this[i].innerHTML = htmlStr;
        }
        return this;
    },
    on: function (type, fn) {
   
   
        const len = this.length;
        for (let i = 0; i < len; i++) {
   
   
            this[i].addEventListener(type, function (e) {
   
   
                fn.call(null, e);
            }, false);
        }
        return this;
    },
});

image.png

let btns = A('.btn');

btns
    .css({
   
   
        padding: '3px 10px',
        borderRadius: '3px',
        borderStyle: 'solid',
        'border-width': '1px',
        'border-color': 'red',
        color: 'red'
    })
    .attr('class', 'btn btn-item')
    .html('按钮喽~~~')
    .on('click', function (e) {
   
   
        console.log(e.target);
    });

/**
 * init(3) [
 *      button.btn.btn-item,
 *      button.btn.btn-item,
 *      button.btn.btn-item,
 *      selector: '.btn',
 *      context: document,
 *      length: 3
 * ]
 */
console.log(btns);

函数、函数简写、箭头函数区别

let obj = {
   
   
    fnA: function () {
   
   
        // console.log(fnA); // Error ---> Uncaught ReferenceError: fnA is not defined
        console.log(this);   // obj
    },
    fnB() {
   
   
        // console.log(fnB); // Error ---> Uncaught ReferenceError: fnB is not defined
        console.log(this);   // obj
    },
    fnC: function fnD() {
   
   
        // console.log(fnC); // Error ---> Uncaught ReferenceError: fnC is not defined
        console.log(fnD);    // ƒ fnD() { ... }
        console.log(this);   // obj
    },
    fnE: () => {
   
   
        // console.log(fnE); // Error ---> Uncaught ReferenceError: fnE is not defined
        console.log(this);   // window
    }
};

obj.fnA();
obj.fnB();
obj.fnC();
obj.fnE();
目录
相关文章
|
5月前
|
设计模式 Java 数据库连接
【设计模式】【创建型模式】工厂方法模式(Factory Methods)
一、入门 什么是工厂方法模式? 工厂方法模式(Factory Method Pattern)是一种创建型设计模式,它定义了一个用于创建对象的接口,但由子类决定实例化哪个类。工厂方法模式使类的实例化延迟
143 16
|
5月前
|
设计模式 负载均衡 监控
并发设计模式实战系列(2):领导者/追随者模式
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发设计模式实战系列,第二章领导者/追随者(Leader/Followers)模式,废话不多说直接开始~
139 0
|
5月前
|
设计模式 监控 Java
并发设计模式实战系列(1):半同步/半异步模式
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发设计模式实战系列,第一章半同步/半异步(Half-Sync/Half-Async)模式,废话不多说直接开始~
130 0
|
5月前
|
设计模式 安全 Java
并发设计模式实战系列(12):不变模式(Immutable Object)
🌟 大家好,我是摘星!🌟今天为大家带来的是并发设计模式实战系列,第十二章,废话不多说直接开始~
106 0
|
5月前
|
设计模式 算法 Java
设计模式觉醒系列(04)策略模式|简单工厂模式的升级版
本文介绍了简单工厂模式与策略模式的概念及其融合实践。简单工厂模式用于对象创建,通过隐藏实现细节简化代码;策略模式关注行为封装与切换,支持动态替换算法,增强灵活性。两者结合形成“策略工厂”,既简化对象创建又保持低耦合。文章通过支付案例演示了模式的应用,并强调实际开发中应根据需求选择合适的设计模式,避免生搬硬套。最后推荐了JVM调优、并发编程等技术专题,助力开发者提升技能。
|
10月前
|
设计模式 前端开发 搜索推荐
前端必须掌握的设计模式——模板模式
模板模式(Template Pattern)是一种行为型设计模式,父类定义固定流程和步骤顺序,子类通过继承并重写特定方法实现具体步骤。适用于具有固定结构或流程的场景,如组装汽车、包装礼物等。举例来说,公司年会节目征集时,蜘蛛侠定义了歌曲的四个步骤:前奏、主歌、副歌、结尾。金刚狼和绿巨人根据此模板设计各自的表演内容。通过抽象类定义通用逻辑,子类实现个性化行为,从而减少重复代码。模板模式还支持钩子方法,允许跳过某些步骤,增加灵活性。
512 11
|
5月前
|
设计模式 Prometheus 监控
并发设计模式实战系列(20):扇出/扇入模式(Fan-Out/Fan-In)(完结篇)
🌟 大家好,我是摘星!🌟今天为大家带来的是并发设计模式实战系列,第二十章,废话不多说直接开始~
172 0
|
8月前
|
JavaScript 前端开发 Docker
如何通过pm2以cluster模式多进程部署next.js(包括docker下的部署)
通过这些步骤,可以确保您的Next.js应用在多核服务器上高效运行,并且在Docker环境中实现高效的容器化管理。
839 44
|
7月前
|
设计模式 Java 关系型数据库
设计模式:工厂方法模式(Factory Method)
工厂方法模式是一种创建型设计模式,通过将对象的创建延迟到子类实现解耦。其核心是抽象工厂声明工厂方法返回抽象产品,具体工厂重写该方法返回具体产品实例。适用于动态扩展产品类型、复杂创建逻辑和框架设计等场景,如日志记录器、数据库连接池等。优点包括符合开闭原则、解耦客户端与具体产品;缺点是可能增加类数量和复杂度。典型应用如Java集合框架、Spring BeanFactory等。
|
9月前
|
设计模式
「全网最细 + 实战源码案例」设计模式——模式扩展(配置工厂)
该设计通过配置文件和反射机制动态选择具体工厂,减少硬编码依赖,提升系统灵活性和扩展性。配置文件解耦、反射创建对象,新增产品族无需修改客户端代码。示例中,`CoffeeFactory`类加载配置文件并使用反射生成咖啡对象,客户端调用时只需指定名称即可获取对应产品实例。
161 40

热门文章

最新文章