目标:用 jQuery 风格封装 DOM
闭包 & 链式操作
链式风格
- 也叫 jQuery 风格
- window.jQuery() 是我们提供的全局函数
- 特殊函数 jQuery
- jQuery(选择器) 用于获取对应的元素
- 但是它不返回这些元素
- 相反,它返回一个对象,称为 jQuery 构造出来的对象
- 这个对象可以操作对应的元素
代码风格的形成
- 我们想获取一个DOM元素,自己封装一个jQuery函数,最开始的做法是直接return获取到的元素
window.jQuery = function(selector){ const elements = document.querySelectorAll(selector) return elements } -
关键:但是 jQuery 的风格就是没有直接 return elements,而是 return 一个对象,并且这个对象具有一些方法,比如添加属性的方法,这个新生成的api可以操作elements,并且这个jQuery函数,return的刚好就是api
window.jQuery = function(selector){ const elements = document.querySelectorAll(selector) // api 可以操作这个elements const api = { // ES6 的简写形式,之前是"addClass": function(className){} addClass(className){ for(let i=0;i<elements.length;i++){ elements[i].classList.add(className) } return undefined } } return api } - 闭包:上面这段代码使用了闭包(即函数访问了外部的变量)
- 上面这段代码返回的是api,而不是返回的是elements
- 当我们调用上面的接口的时候,会给元素添加属性
const api = jQuery('.test') // 不返回元素们,返回 api 对象 api.addClass('red') // 遍历所有刚才获取的元素,添加 .red -
关键:我们之前的代码是 api 里面的函数对象,返回的undefined,那么能不能返回api呢?可以
window.jQuery = function(selector){ const elements = document.querySelectorAll(selector) const api = { addClass(className){ for(let i=0;i<elements.length;i++){ elements[i].classList.add(className) } // 直接返回这个对象本身 return api } } return api } -
链式操作:这意味着我们在使用了api的对象函数后,拿到的还是一个api对象,那么这样就可以进行链式操作
const api = jQuery('.test') // 不返回元素们,返回 api 对象 api.addClass('red').addClass('blue').addClass('green') // 链式操作 - 我们之前有一个概念:函数如果使用一个对象来调用,那么这个函数的this就是调用函数的对象,也就是obj.fn(p1)等价于obj.fn.call(obj, p1);我们针对这个概念对代码进行改进
window.jQuery = function(selector){ const elements = document.querySelectorAll(selector) const api = { addClass(className){ for(let i=0;i<elements.length;i++){ elements[i].classList.add(className) } // 改进 return this } } // 这边不能动 return api } - api是我们定义的,我们能不能直接return?可以的
window.jQuery = function(selector){ const elements = document.querySelectorAll(selector) return { addClass(className){ for(let i=0;i<elements.length;i++){ elements[i].classList.add(className) } return this } } } - 总结:jQuery的核心思想是
- 闭包:提供一个函数,这个函数接收一个选择器,jQuery将根据选择器获取这些元素,但是不会去返回元素,而是返回对象,对象中有对应的方法,这些方法来操作元素,jQuery用闭包去维持elements与函数
- 链式操作:函数在调用的时候充分利用了this形成了链式操作
实现 find 函数
我们通常所说的jQuery对象指的是jQuery构造出来的对象
jQuery是构造函数吗?
- 是
- 因为 jQuery函数确实构造出了一个对象
- 不是
- 因为不需要写new jQuery()就能构造出来一个对象
- 以前见到的构造函数必须结合 new 才行
- 结论
- jQuery 是一个不需要加 new 的构造函数
- jQuery 不是常规意义上的构造函数
链式风格
- 查
- jQuery('#xxx')返回并不是一个元素,而是一个api对象
- jQuery('#xxx').find('.red')查找#xxx元素中的.red元素
- jQuery('#xxx').parent()获取爸爸
- jQuery('#xxx').children()获取儿子
- jQuery('#xxx').siblings()获取兄弟
- jQuery('#xxx').index()获取排行老几(从0开始)
- jQuery('#xxx').next()获取弟弟
- jQuery('#xxx').prev()获取哥哥
- jQuery('#xxx').each(fn)遍历并对每一个元素执行fn
实现find
- 这段代码添加了find方法,但是这个find方法返回的是数组,无法进行链式操作
- 如果我们将array改成this,其实在调用的时候,返回的是elements
window.jQuery = function(selector){ const elements = document.querySelectorAll(selector) return { addClass(className){ for(let i=0;i<elements.length;i++){ elements[i].classList.add(className) } return this }, find(selector){ let array = [] for(let i=0;i<elements.length;i++){ // 伪数组要用Array.from() const elements2 = Array.from(elements[i].querySelectorAll(selector)) array = array.concat(elements2) } return this } } } - 我们能不能将elements直接改成array?不可以,一旦修改,会影响之前我们对elements的操作
window.jQuery = function(selector){ let elements = document.querySelectorAll(selector) return { addClass(className){ for(let i=0;i<elements.length;i++){ elements[i].classList.add(className) } return this }, find(selector){ let array = [] for(let i=0;i<elements.length;i++){ // 伪数组要用Array.from() const elements2 = Array.from(elements[i].querySelectorAll(selector)) array = array.concat(elements2) } eleemnts = array return this } } } - 我们需要对jQuery重新进行封装
- 我们对于api要重新构造,这个新的api要靠jQuery重新构造
- jQuery不能只接收选择器,还需要接收数组。
- 如果接收的是数组就让elements等于这个数组
window.jQuery = function(selectorOrArray){ let elements if(typeof selectorOrArray === 'string'){ elements = document.querySelectorAll(selectorOrArray) }else if(typeof selectorOrArray instanceof Array){ elements = selectorOrArray } return { addClass(className){ for(let i=0;i<elements.length;i++){ elements[i].classList.add(className) } return this }, find(selector){ let array = [] for(let i=0;i<elements.length;i++){ const elements2 = Array.from(elements[i].querySelectorAll(selectorOrArray)) array = array.concat(elements2) } // 返回一个新的api return jQuery(array) } } }
实现end函数
- 就是回到上一级的elements
- 我们将之前的api记录下来
window.jQuery = function(selectorOrArray){ let elements if(typeof selectorOrArray === 'string'){ elements = document.querySelectorAll(selectorOrArray) }else if(typeof selectorOrArray instanceof Array){ elements = selectorOrArray } return { // 直接将oldApi记录下来 oldApi: selectorOrArray.oldApi, addClass(className){ for(let i=0;i<elements.length;i++){ elements[i].classList.add(className) } return this }, find(selector){ let array = [] for(let i=0;i<elements.length;i++){ const elements2 = Array.from(elements[i].querySelectorAll(selectorOrArray)) array = array.concat(elements2) } // 将当前操作的api放入数组中,作为记录保存下来 array.oldApi = this return jQuery(array) }, end(){ // 直接return就好 return this.oldApi } } } ```
实现其他的一些操作
window.jQuery = function(selectorOrArrayOrTemplate){
let elements
if(typeof selectorOrArrayOrTemplate === 'string'){
if(selectorOrArrayOrTemplate[0] === '<'){
// 创建div
elements = [createElement(selectorOrArrayOrTemplate)]
}else{
// 查找div
elements = document.querySelectorAll(selectorOrArrayOrTemplate)
}
}else if(typeof selectorOrArrayOrTemplate instanceof Array){
elements = selectorOrArrayOrTemplate
}
function createElement(string){
const container = document.createElement("template");
container.innerHTML = string.trim();
return container.content.firstChild;
}
return {
// 标记
jquery: true,
elements: elements,
get(index){
return elements[index]
},
appendTo(node){
if(node instanceof Element){
this.each(el => node.appendChild(el))
}else if(node.jquery === true){
this.each(el => node.get(0).appendChild(el))
}
},
append(children){
if(node instanceof Element){
this.get(0).appendChild(children)
}else if(children instanceof HTMLCollection){
for(let i=0;i<children.length;i++){
this.get(0).appendChild(children[i])
}
}else if(children.jquery === true){
children.each(node => this.get(0).appendChildren(node))
}
},
find(selector){
let array = []
for(let i=0;i<elements.length;i++){
const elements2 = Array.from(elements[i].querySelectorAll(selectorOrArrayOrTemplate))
array = array.concat(elements2)
}
array.oldApi = this
return jQuery(array)
},
each(fn){
for(let i=0;i<elements.length;i++){
fn.call(null, elements[i], i)
}
return this
},
parent(){
const array = []
this.each((node) => {
if(array.indexOf(node.parentNode) === -1){
array.push(parentNode)
}
})
return jQuery(array)
},
children(){
const array = []
this.each((node) => {
if(array.indexOf(node.parentNode) === -1){
array.push(...node.children)
}
})
},
print(){
console.log(elements)
},
addClass(className){
for(let i=0;i<elements.length;i++){
elements[i].classList.add(className)
}
return this
},
oldApi: selectorOrArrayOrTemplate.oldApi,
end(){
return this.oldApi
}
}
}
// 简写
window.$ = window.jQuery
命名风格
- 下面的代码令人费解
- const div = $('div#test')
- 我们会误以为div是一个DOM
- 实际上div是jQuery构造的api对象
怎么避免这种误解呢?
- 改成这样
- const
('div#text')
- $div.appendChild 不存在,因为它不是DOM对象
- $div.find存在,因为它是jQuery对象
- const
总结
这是一种什么感觉
- 就感觉DOM是不可见的
- 你不需要知道DOM的任何细节
- 只需要使用简洁的API即可
- 一个好的封装,能让使用者完全不知道内部的细节
- 这是通过闭包实现的