链式调用的优势
一行代码完成多个操作,代码简洁、易读,避免定义多个中间变量等。
js
T('#target').css({'background-color': 'red'}).attr('class', 'demo').html('demo text show').on('click', function() { console.log('clicked') });
链式调用的实现思路

分析实现思路
从结果 T('#target').x().y()..
来看,T
应该是个函数,调用这个函数返回了一个包含所有属性和方法的对象 (调用任何方法也是)。所以一定有一个对象空间 存储和共享 所有的属性和方法。不禁想到了原型空间 prototype
, prototype
是对象的特殊属性,在js中 function
也是对象,所以返回自己的原型空间就可以了。

我们还需要创建一个属性 assistant
(助手),来一同保存 T.prototype
这个指针
js
T.assistant = T.prototype = {
name: 'Tagawa',
init: function() {
...
},
sayName: function() {
return this.name;
}
}
js
function T(target) {
return T.assistant.init(target)
}
初始化
从 T(arguments)
来看,内部有一个 init
初始化的方法,假设这个方法只能通过 id
获取 dom
元素
js
T.assistant = T.prototype = {
name: 'Tawaga',
init: function(target) {
return document.getElementById(target)
}
}
const hello = T('hello')
console.log('hello: ', hello);
最重要的是它要返回
当前对象
(调用者)
从控制台可以看到,获取到了dom元素,但是 init
方法没有返回 this
,这才是重点 ,没有 this
就无法 链式调用 ,所以在 init
方法内把获取到的 dom
元素存储到新属性里 [0]
js
init: function(target) {
this[0] = document.getElementById(target);
this.length = 1;
return this;
},
但当你测试的时候,你发现第二次打印出来的 hello
元素,被覆盖成了 box
js
const hello = T('hello')
console.log('hello: ', hello);
const box = T('box')
console.log('hello: ', hello);
因为你修改了共同的
this
对象中的 this[0]
实例化
解决这个问题要实例化 init
js
const T = function(target) {
return new T.assistant.init(target);
};
T.assistant = T.prototype = {
constructor: T,
name: 'Tawaga',
init: function(target) {
this[0] = document.getElementById(target);
this.length = 1;
return this;
},
sayName: function() {
return this.name;
}
}
当再打印测试时,你发现一切正常了
js
const hello = T('hello')
console.log('hello: ', hello);
const box = T('box')
console.log('box: ', box);
console.log('hello: ', hello);
但是当你尝试 链式调用 的时候你发现又报错了
js
console.log(T('hello').sayName());

那是因为,当 new T.assistant.init(target)
后,init
方法内部 return 的 this
,这个 this
是 init
对象,此时的 init
相当于构造函数 ,然而不论是在 init
方法内部,还是在 init.prototype
中都没有 sayName
这个方法,所以要把 T.prototype
的指针也给 init.prototype
一份

js
T.assistant.init.prototype = T.assistant;
再测试一下,发现链式调用是ok的,控制台输出了预期的信息

用 extend 拓展
可拓展的能力是很强大的,T
应该提供一个可供外部插入能力 的入口方法extend
,用户可以根据自己的业务需求 渐进增强 T
js
T.extend = T.assistant.extend = function() {
let len = arguments.length, target = arguments[0];
if (len === 1) {
// 内部(插件)
target = this;
}
// 外部(工具)
for (let i = 0; i < len; i++) {
for (let j in arguments[i]) {
target[j] = arguments[i][j]
}
}
return target;
}
当传入1个参数时,就是给 T
内部拓展属性(插件模式),当传入多个参数时,就是给第1个参数拓展属性(工具模式)
js
// 内部插件拓展到T.assistant
T.assistant.extend({ innerHello: function() { return this.name }});
const out = T.extend({name: 'tgg'}, {age: 18}, {like: 'code'});
console.log('inner: ', T('#hello').innerHello());
console.log('out: ', out);
完整代码
js
const T = function(target) {
return new T.assistant.init(target);
}
T.assistant = T.prototype = {
constructor: T,
name: 'Tawaga',
init: function(target, context) {
this.length = 0;
context = context ?? document;
if (target.startsWith('#')) {
const element = context.getElementById(target.slice(1));
this[0] = element;
} else {
const elements = context.getElementsByTagName(target);
for (let i = 0; i < elements.length; i++) {
this[i] = elements[i];
}
this.length = elements.length;
}
this.target = target;
this.context = context;
return this;
},
sayName: function() {
return this.name;
},
push: [].push,
sort: [].sort,
splice: [].splice
}
T.assistant.init.prototype = T.assistant;
T.extend = T.assistant.extend = function() {
let len = arguments.length, target = arguments[0];
if (len === 1) {
// 内部(插件)
target = this;
}
// 外部(工具)
for (let i = 0; i < len; i++) {
for (let j in arguments[i]) {
target[j] = arguments[i][j]
}
}
return target;
}
// 内部插件拓展到T.assistant
T.assistant.extend({ innerHello: function() { return this.name }});
const out = T.extend({name: 'tgg'}, {age: 18}, {like: 'code'});
console.log('inner: ', T('#hello').innerHello());
console.log('out: ', out);
实战
添加事件
js
T.assistant.extend({
// 添加事件
on: (function() {
// 标准浏览器DOM2级事件
if(document.addEventListener) {
return function(type, fn) {
const i = this.length - 1;
for (; i >= 0; i--) {
this[i].addEventListener(type, fn, false);
}
// 返回原对象
return this;
}
} else if (document.attachEvent) {
return function(type, fn) {
const i = this.length - 1;
for (; i >= 0; i--) {
this[i].addEvent('on' + type, fn);
}
return this;
}
// 不支持 DOM2 级事件浏览器添加事件
} else {
return function(type, fn) {
const i = this.length - 1;
for (; i >= 0; i--) {
this[i]['on' + type] = fn;
}
return this;
}
}
})()
})
callback篇头
js
T('#target').css({'background-color': 'red'}).attr('class', 'demo').html('demo text show').on('click', function() { console.log('clicked') });