模拟 jQuery API的实现
jQuery 是什么?
jQuery实质上是一个构造函数,接受一个参数,这个参数可能是节点,然后返回一个方法对象去操作节点。官方文档是这样说明的:
jQuery是一个快速,小巧,功能丰富的JavaScript库。它通过易于使用的API在大量浏览器中运行,使得HTML文档遍历和操作,事件处理,动画和Ajax变得更加简单。
那么今天我们就来演示一下jQuery API的工作原理
用原生DOM写一个类似jQuery的API
1.写一个带有id的 ul 列表
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport"
content="width=device-width,
initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>模拟 jQuery API的实现</title>
</head>
<body>
<ul>
<li id="item1">选项1</li>
<li id="item2">选项2</li>
<li id="item3">选项3</li>
<li id="item4">选项4</li>
<li id="item5">选项5</li>
<li id="item6">选项6</li>
</ul>
<script>
</script>
</body>
</html>
2. 以item3为节点,找到其兄弟节点
通过 var allChildren = item3.parentNode.children 获取 item3 父节点的所以子节点,然后遍历所有子节点,得到 item3以外 的所有节点,这样就找到选项3的所以兄弟节点啦。可以 console.log一下。
(由于array是伪数组,不能用push的方法,所以我们用到 array[array.length] = allChildren[i] 的方法)
<script>
var allChildren = item3.parentNode.children
var array = {length:0}
for(let i = 0; i < allChildren.length; i++){
if(allChildren[i] !== item3){
array[array.lenth]=allChildren[i]
array.length += 1
}
}
console.log(array)
</script>
3. 代码封装
封装的好处有很多:给代码一个名字方便调用;形成局部变量可以避免覆盖JS原始变量(立即调用函数)等
给这个函数取个名字,如 getSiblings;把 item3 换成 node,这样输入任意节点都可以使用这个函数了;注意不要忘记 return,这样我们就得到了一个函数 function getSiblings(node){}
<script>
function getSiblings(node){
var allChildren = node.parentNode.children
var array = {length:0}
for(let i = 0; i < allChildren.length; i++){
if(allChildren[i] !== node){
array[array.lenth]=allChildren[i]
array.length += 1
}
}
return array
}
</script>
4. 封装函数:function addClass(node, classes){}
现在我们要给 item3 加 class属性
首先我们声明一个 classes 对象,里面有 a、b、c 三个属性;同时分别给它们一个布尔值,方便 add 和 remove;遍历各个属性。
var classes = {'a':false, 'b':true, 'c':true}
for(let key in classes){
var value = classes[key]
var methodName = value?'add':'remove';
item3.classList[methodName](key)
}
可以看到,class b、c已经被添加到 item3 中了
同样我们来封装一下这些代码,如下所示:
function addClass(node, classes){
for (var key in classes){
var value = classes[key]
var methodName = value ? 'add' : 'remove'
//console.log (methodName )
//console.log (node.classList)
//console.log (node.classList.add)
//console.log (node.classList[methodName])
node.classList[methodName](key)
}
}
/*
obj.x() 等同于 obj['x']()
注意一点上述代码不能用点运算符,要用[]运算符,classList['add'] === classList.add
*/
addClass(item3, {a:true, b:false, c:true})
现在,只要你给一个 node 和 classes 于此函数,就可以给 该节点添加 classes所包含的正确属性
5.命名空间:
给封装的函数一个名字,方便其他人使用,同时防止与前人命名的冲突。
假如我们想把这两个封装好好的函数联系到一起,以便以后调用的话,我们可以这样去写。
window.xxdom = {}
xxdom.getSiblings = function (node) {
var allChildren = node.parentNode.children
var array = {
length: 0
}
for (let i = 0; i < allChildren.length; i++) {
if (allChildren[i] !== node) {
array[array.length] = allChildren[i]
array.length += 1
}
}
return array
}
xxdom.addClass = function (node, classes) {
classes.forEach( (value) => node.classList.add(value) )
}
xxdom.getSiblings(item3)
xxdom.addClass(item3, ['a','b','c'])
6.能不能把node 放在前面
node.getSiblings()
node.addClass()
我们发现当我们要用到上面的命名空间的时候会非常麻烦,我们总是要用 dom.getSiblings()、xxdom.addClass()、总是要带有别人的一个小尾巴。 item3.getSiblings()、item3.addClass() 好像看起来更清爽一些,有没有办法来实现呢?
其实是有的。为了验证,我们给 Node 的共有属性添加 getSiblings属性,然后我们测试下能否访问到:
Node.prototype.getSiblings = function(){
return 1
}
现在有个问题,我们这里的getSiblings()怎么获取到item3 ?
方法一:扩展 Node 接口
直接在 Node.prototype 上加函数
Node 如何取到 item3?
用 this
!
why?把上面写成 .call 的形式,因为this 是call 的第一个参数。
那么 item3 为什么会有 getSiblings属性呢?
因为我们篡改了其 proto 最终指向的 node.prototype 的共有属性,然后添加了一个 getSiblings的方法,然后它里面呢先去获取this 的父节点的所有儿子,那么 this 是谁呢?this 就是你在调用的时候就会帮你把 item3给传递进来,item3会自动的传给 getSiblings() 的 this,所以 this 就是.前面的东西,不管你是什么。然后声明一个伪数组,遍历这个伪数组,如果伪数组里面的第[i]项全不等于 this (这时候的 this 就是 item3),那么不等于 item3的 item放到伪数组里面,伪数组的 length +1,循环后返回这个伪数组,于是我们得到了 item1、item2、item4、item5。
** 自己命名新的接口 Node2**
前面写的内容其实不是很好,为什么呢?
因为我们在改 node 的共用属性。比如 A 同学在上面添加了2个函数,B 同学也在上面添加了2个函数,然后我们也添加了2个函数,所以就存在被覆盖的可能性。
那, 如果不改原型,我们怎么能实现 item3.getSiblings呢?方法也是有的!就是命名新的接口,示例如下:
function Node2(node){
return {
element: node,
getSiblings: function(){
},
addClass: function(){
}
}
}
let node =document.getElementById('x')
let node2 = Node2(node)
node2.getSiblings()
node2.addClass()
真实代码(这种对 Node 没有产生破坏的方法叫做「无侵入」)如下:
window.Node2 = function (node) {
return {
getSiblings: function () {
var allChildren = node.parentNode.children
var array = {
length: 0
}
for (let i = 0; i < allChildren.length; i++) {
if (allChildren[i] !== node) {
array[array.length] = allChildren[i]
array.length += 1
}
}
return array
},
addClass: function (classes) {
for (var key in classes) {
var value = classes[key]
var methodName = value ? 'add' : 'remove'
node.classList[methodName](key)
}
}
}
}
7. 把 Node2 改成jQuery
通过上面的操作,我们就在原有 Node 的基础上新扩展了接口,我们这时候就可以给新扩展的起个自己喜欢的名字,就叫jQuery吧
function jQuery(node) {
return {
element: node,
getSiblings: function () {
},
addClass: function () {
}
}
}
let node = document.getElementById('x')
let node2 = jQuery(node)
node2.getSiblings()
node2.addClass()
实际操作如下:
window.jQuery = function (nodeOrselector) {
let nodes = {}
if (typeof nodeOrselector === 'string') {
let temp = document.querySelectorAll(nodeOrselector) //伪数组
for (let i = 0; i < temp.length; i++) {
nodes[i] = temp[i]
}
nodes.length = temp.length
} else if (nodeOrSelector instanceof node) {
node = {
0: nodeOrSelector,
length: 1
}
}
nodes.getSiblings = function () {
var allChildren = node.parentNode.children
var array = {
length: 0
}
for (let i = 0; i < allChildren.length; i++) {
if (allChildren[i] !== node) {
array[array.length] = allChildren[i]
array.length += 1
}
}
return array
}
nodes.addClass = function (classes) {
classes.forEach((value) => {
for (let i = 0; i < nodes.length; i++) {
node[i].calssList.add(value)
}
})
}
return nodes
}
window.$ = jQuery //缩写吧:alias
var $div = $('div')
$div.addClass('red') // 可将所有 div 的 class 添加一个 red
$div.setText('hi') // 可将所有 div 的 textContent 变为 hi
8. $缩写与alias
window.$ = jQuery
即var node2 = $(node)
但是为了防止记混 node2 到底有没有引入 jQuery
大家通常这样写:
var $node2 = $(node)
我们会在引用的 jQuery标签前面加上$
符号来区分原生的标签或者接口,看到$
我们立马就明白了这是由 jQuery 引用染生出来的。
到这里我们已经知道 jQuery 是个什么了:它就是一个函数,是 JS 原始 DOM 的扩展,便于我们更好得使用JS写代码的加强版 DOM API。