目录

  • jQuery 如何获取元素
  • jQuery 的链式操作是怎样的
  • jQuery 如何创建元素
  • jQuery 如何移动元素
  • jQuery 如何修改元素的属性

  • 选择网页元素
  • 改变结果集
  • 链式操作
  • 元素的操作:取值和赋值
  • 元素的操作:移动
  • 元素的操作:复制、删除和创建
  • 工具方法
  • 事件操作
  • 特殊效果

正文


链式风格(jQuery)

window.jQuery()是所提供的全局函数

用jQuery风格重新封装DOM

特殊函数jQuery

  • jQuery(选择器)用于获取选择器所对应的元素
  • 但却不返回这些元素
  • 而是返回一个对象,称为 jQuery构造出来的对象,即 API
  • 这个对象可以操作对应的元素
  • 代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/*
<body>
  <div class="red">1</div>
  <div class="red">2</div>
  <div class="red">3</div>
</body>
*/
window.jQuery = function(选择器){
  const nodes = document.querySelector(选择器)
  // nodes 是伪数组,要转成数组
  const array = Array.from(nodes)
  // api 可以操作 array
  const api = {
    log(){
      console.log(array)
    }
  }
  return api // 不 return array 而是 return api
}

// 由于用了querySelector, 也是静态的,不实时变化的

jQuery是构造函数吗?

  • 是,因为jQuery函数确实构造出了一个对象
  • 又不是,因为不需要写new jQuery()就能构造一个对象,常规构造函数用 new操作符

结论

  • jQuery是一个不需要加new的构造函数
  • jQuery不是常规意义上的构造函数

jQuery对象 代指jQuery函数构造出来的对象(*可以认为是被省略掉的api)

  • 口头约定,jQuery对象并非「jQuery这个对象」

一些必须搞清的术语

  • Object是个函数
  • Object对象表示Object构造出的对象
  • Array是个函数
  • Array对象/数组对象表示Array构造出来的对象
  • Function是个函数
  • Function对象/函数对象表示Function构造出来的对象

jQuery.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
window.jQuery = function (selector) {
    // this.console.log('我是jQuery')
    // 核心代码 接受一个选择器;
    // 根据选择器得到 一些元素;
    // 返回一个对象 这个对象 上有个方法 可以操作 这些元素
    const elements = document.querySelectorAll(selector)
    // return elements // 不返回此对象
    // 而是返回一个 API 可以操作 elements
    /*
    const api_test = {
            //ES6
            doSth() {
                console.log(elements) // 闭包:函数访问外部 变量
            },
            // ES5
            "doOther": function () {
                console.log(elements)
            }
        }
    */
    /*
    const api = {
        addClass(className) {
            // 遍历所有刚才获取的elements 元素,添加 .className 样式
            for (let i = 0; i < elements.length; i++) {
                elements[i].classList.add(className)
            }
            return this // api
        }
    }
        return api // 这里的 this 是 window // window.jQuery
        // api 的名字其实没有必要 直接返回对象
     */
    return {
        addClass(className) {
            /* 遍历所有刚才获取的elements 元素,添加 .className 样式 */
            for (let i = 0; i < elements.length; i++) {
                elements[i].classList.add(className)
            }
            return this
        }
    }
}

api 的名字没有必要 直接返回对象本,api 对象在外部的环境中创建

JQ的核心思想

  • 提供一个函数 这个函数接收一个CSS选择器
  • 通过选择器,获取到一些元素 但并不返回元素
  • 而是返回一个对象 这个对象里包含一些方法(函数)
  • 这些方法(函数)去操作获取到的元素
  • 即用闭包维持 函数外的变量 防止被回收机制删除
  • 创建的JQ对象 api 作为返回值 从前面传递到后面 来链式调用
  • 调用后 才知道 this 的具体指向
  • 直接创建匿名的对象 进行链式操作 声明一个对象紧接再调用它 且只用到一次 省略变量名
  • 省略中间过程 留下最简练代码

链式风格各功能接口:增删改查

接口

1
2
3
4
5
6
7
8
9
jQuery('#xxx') // 返回值并不是元素,而是一个`API`对象
jQuery('#xxx').find('.red') // 查找`#xxx`里的`.red`元素
jQuery('#xxx').parent() // 获取父元素
jQuery('#xxx').children() // 获取子元素
jQuery('#xxx').siblings() // 获取兄弟元素
jQuery('#xxx').index() // 获取排列下标
jQuery('#xxx').next() // 获取下一个元素
jQuery('#xxx').prev() // 获取上一个
jQuery('.red').each(fn) // 遍历并且对每个元素执行`fn`

实现find()

jQuery.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
window.jQuery = function (selectorOrArray) {
    /* 重载判断
     ** selectorOrArray不仅接受选择器 还有数组
     */
    let elements // 作用域的提升  用const声明的的时候必须直接赋值且不能再改
    if (typeof selectorOrArray === 'string') {
        // const elements = document.querySelectorAll(selectorOrArray)
        elements = document.querySelectorAll(selectorOrArray)
    } else if (selectorOrArray instanceof Array) {
        // const elements = selectorOrArray // 作用域的提升
        elements = selectorOrArray
    }
    return {
        find(selector) {
            let arr = []
            for (let i = 0; i < elements.length; i++) {
                const elements2 = Array.from(elements[i].querySelectorAll(selector))
                arr = arr.concat(elements2)
            }
            // elements = arr
            // return this // 返回的是数组 无法进行链式调用
            // return newApi // 不能用原来的api对象 需要更新 api 引用
            //
            // const newApi = jQuery(arr) // 不能用原来的api对象,需要用jQuery构造一个新的api且接受传入的数组
            // return newApi
            return jQuery(arr) // 简化
        },
        addClass(className) {
            /* 遍历所有刚才获取的elements 元素,添加 .className 样式 */
            for (let i = 0; i < elements.length; i++) {
                elements[i].classList.add(className)
            }
            return this
        }
    }
}

当在改变element的时候,会影响之前所有保留 api 对象的引用

为了不改变api的引用,不能用原来的api对象,需要用jQuery构造一个新的api const newApi = jQuery(arr);return newApi,避免污染之前的api

  • 在方法中,重新封装一个 jQuery 函数
  • jQuery函数获取一些元素,把这些元素递给 jQuery
  • 让 jQuery 不仅接受选择器,同样可以接受一个数组
  • 将数组重新传给 jQuery,封装一个新的 api
  • 新的api 结构和之前一样,但保存的 elements 不同
  • 得到一个新的 api 对象,这个对象用来操作arr
  • jQuery 传什么(对象) ,就返回一个对象操作什么(对象)
  • return jQuery(arr) 类似递归的用法

实现回溯操作.end()

  • return jQuery(arr)目前只得到要操作的是 arr
  • 要传入之前的元素,作为arr的属性 arr.OldApi = this // this就是api
  • .end()要读取

jQuery.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
return {
    oldApi: selectorOrArray.oldApi, // 将oldApi属性加到selectorOrArray上
    find(selector) {
        let arr = []
        for (let i = 0; i < elements.length; i++) {
            const elements2 = Array.from(elements[i].querySelectorAll(selector))
            arr = arr.concat(elements2)
        }
        arr.oldApi = this // this是旧的api
        return jQuery(arr) // 简化
    },
    end(){
        return this.oldApi // this是新的api2
    }
}

main.js

1
2
3
4
// const api1 = jQuery('.test')
// const api2 = api1.find('.child').addClass('red').addClass('blue')
// const oldApi = api2.end().addClass('yellow')
jQuery('.test').find('.child').addClass('red').addClass('blue').end().addClass('yellow')

实现遍历each()

main.js

1
2
3
4
const xx = jQuery('.test').find('.child')
x.each((div) => {
    console.log(div)
})

jQuery.js

1
2
3
4
5
6
each(fn) { // 遍历当前所有元素
    for (let i = 0; i < elements.length; i++) {
        fn.call(null, elements[i], i) // x.each((div)里的 div 就是第一个形参 即 elements[i],先忽略 i
    }
    return this // this就是这个被省略掉的api
}

实现打印当前节点print()

1
2
3
print() {
    console.log(elements) // elements就是对应的元素们
}

实现查父元素parent()

  • 借用each()遍历,获取父元素

jQuery.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
parent() {
    const array = []
    /* this 就是当前这个被省略掉的api */
    this.each((node) => { // 遍历 每次 对于每个元素 获取父节点 把不同的放入数组
        /* 不重复同样的节点 */
        // if (array.indexOf(node.parentNode) >= 0) // 第0个 或 第一个 有就不push
        if (array.indexOf(node.parentNode) === -1) { // 第0个 或 第一个 有就不push
            array.push(node.parentNode) // 没有才push
        }
    })
    return jQuery(array)
}

实现查子元素children()

jQuery.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
children() {
    const array = []
    /* this 就是当前这个被省略掉的api */
    this.each((node) => { // 遍历 每次 对于每个元素 获取子节点 把不同的放入数组
        // array.push(node.children) // 没有把数组摊平 有不同结构
        array.push(...node.children) // 展开操作符... 拆开所有元素 并且合并为一个数组
        // 等价于 array.push(node.children[0], node.children[1],...[2]...)
    })
    return jQuery(array)
}

main,js

1
2
const y = jQuery('.test')
y.children().print()

each()和``…`展开操作符简化代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
window.jQuery3 = function (selectorOrArray) {
    let elements
    if (typeof selectorOrArray === 'string') {
        elements = document.querySelectorAll(selectorOrArray)
    } else if (selectorOrArray instanceof Array) {
        elements = selectorOrArray
    }
    return {
        addClass(className) {
            this.each(n => n.classList.add(className))
        },
        find(selector) {
            let array = []
            this.each(n => {
                array.push(...n.querySelectorAll(selector))
            })
            arr.oldApi = this
            return jQuery(array)
        },
        parent() {
            const array = []
            this.each(n => {
                if (array.indexOf(n.parentNode) === -1) {
                    array.push(n.parentNode)
                }
            })
            return jQuery(array)
        },
        children() {
            const array = []
            this.each(n => {
                array.push(...n.children)
            })
            return jQuery(array)
        },
        each(fn) {
            for (let i = 0; i < elements.length; i++) {
                fn.call(null, elements[i], i)
            }
            return this
        }
        print() {
            console.log(elements) // elements就是对应的元素们
        },
        oldApi: selectorOrArray.oldApi, // 将oldApi属性加到selectorOrArray上
        end() {
            return this.oldApi // this是新的api2
        }
    }
}

太长TL,DR

  • window.$ = window.jQuery
  • 类似bash alias,添加别名

命名风格

下面的代码令人误解

  • const div = $('div#test')
  • 不小心写成div.appendChild(xxx)
  • 可能误认为div是一个DOM,但并不是div元素 而是 api 对象
  • 实际上divjQuery构造的API对象
  • DOM 对象只能使用 DOM 的 API,比如 querySelector appendChild
  • jQuery 对象只能使用 jQuery 的 API, 比如 each find

避免误解的方法

  • DOM 对象使用el开头,如 elDiv 来命名
  • jQuery 对象使用$开头,如 $div 来命名
  • const $div = $('div#test')
  • $div.appendChild不存在,因为它不是DOM对象
  • $div.find存在,因为它是jQuery对象

约定俗成

  • 所有代码中以$开头的变量都是jQuery对象

实现$('#xxx').siblings() 获取兄弟元素

实现$('#xxx').index() 获取排列下标

实现$('#xxx').next() 获取下一个元素

实现$('#xxx').prev() 获取上一个


查·改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$('#xxx') // 返回值并不是元素,而是一个`API`对象
$('#xxx').find('.red') // 查找`#xxx`里的`.red`元素
$('#xxx').parent() // 获取父元素
$('#xxx').children() // 获取子元素
$('#xxx').siblings() // 获取兄弟元素
$('#xxx').index() // 获取排列下标
$('#xxx').next() // 获取下一个元素
$('#xxx').prev() // 获取上一个
$('.red').each(fn) // 遍历并且对每个元素执行`fn`
$('#xxx').end() // oldApi: selectorOrArray.oldApi

1
$('<div><span>1</span></div>').appendTo(document.body)
  • JS的函数重载
  • 一个函数可以接受不同类型的参数:代码链接
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
<body>
  <div class="red">1</div>
  <div class="red">2</div>
  <div class="red">3</div>
</body>
*/
window.jQuery = function(选择器或标签) {
  if (选择器或标签.trim().indexOf('<') === 0) {
    // 说明是标签
  } else {
    // 说明是选择器
    const nodes = document.querySelector(选择器)
      // nodes 是伪数组,要转成数组
    const array = Array.from(nodes)
      // api 可以操作 array
    const api = {
      log() {
        console.log(array)
      }
    }
    return api // 不 return array 而是 return api
  }
}
  • 返回的并不是新增的元素,而是API对象
1
$('<div><span>1</span></div>').appendTo(document.body)
  • appendTo()可以吧新增的元素放到另一个元素里

感觉DOM是不可见的

不需要知道DOM的任何细节

只需要使用简洁的API即可

一个好的封装,能让使用者完全不知道内部细节

这是通过闭包实现的


细节

举例1

1
const $div = $('div#test')
  • $div并不是DOM对象,而是jQuery构造的API对象
  • 可否从$div得到div元素?
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$div.get(0) // 获取第0个元素
// div
$div.get(1) // 获取第1个元素
// undefined
$div.get(2) // 获取第2个元素
// undefined
$div.size() // 获取元素的个数
$div.length // 也可以获取元素的个数
$div.get() // 获取所有元素组成的数组
// [div]

举例2

1
const $div = $('.red') // 假设有3个 `div.red`
  • $div并不是DOM对象们,而是jQuery构造的API对象们
  • 可否从$div得到3个div元素?
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$div.get(0) // 获取第0个元素
// div
$div.get(1) // 获取第1个元素
// undefined
$div.get(2) // 获取第2个元素
// undefined
$div.size() // 获取元素的个数
$div.length // 也可以获取元素的个数
$div.get() // 获取所有元素组成的数组
// [div, div, div]

$div.get(0)太麻烦

  • 改成$div[0]获取第一个 div

jQuery链式风2.0

增+

1
2
3
4
5
6
$('body') // 获取 `document.body`
$('body').append($('<div>1</div>')) // 添加子节点
$('body').append('<div>1</div>') // 更方便
$('body').preppend(div || $div) // 添加第一个子节点
$('#test').after(div || $div) // 向后追加节点
$('#test').before(div || $div) // 向前追加节点

删-

1
2
$div.remove()
$div.empty()

1
2
3
4
5
6
7
8
9
$div.text(?) // 读写文本内容
$div.html(?) // 读写HTML内容
$div.attr('title', ?) // 读写文本内容
$div.css({color: 'red'}) // 用来操作内联的style 读写 `style || $style`更好
$div.addClass('blue') //
$div.removeClass('blue') //
$div.hasClass('blue') //
$div.on('click', fn)
$div.off('click', fn)

注意:$div可能对应到多个div元素,即默认获取的div或其他元素是(伪)数组,每部操作都要遍历


后续

使用原型

  • 把共用属性(函数)全都放到$.prototype
  • $.fn = $.prototype,解决命名太长
  • api.__proto__指向$.fn

公开代码

  • 发布到GitHub
  • 添加MD文档
  • want star
  • get blame

JQ Usage


设计模式

jQuery用到了那些设计模式

  • 不用new的构造函数,这个模式没有专门的名字
  • $(多种参数),这个模式叫重载
  • 用闭包隐藏细节,这个模式没有专门的名字
  • $div.text()即可读,又可写,getter/setter
  • $.fn作为$.prototype的别名,即别名(模式)
  • jQuery可针对不同浏览器使用不同代码,这个叫适配器

设计模式就是对通用代码取个名字

设计模式用后总结的,抽象出代码中好的地方,复用


通过jQuery学封装技巧,帮助理解vue/react

jQuery 都过时了,那我还学它干嘛?

学习路线

  • 理解jQuery原理
  • 使用jQuery做一两个项目
  • 总结一篇博客
  • vuereact
  • 找工作


参考文章

封装DOM.pdf

jQuery中的设计模式.pdf

GitHub Repo源代码链接

jQuery API 中文文档

课程中的代码:https://github.com/FrankFang/dom-2

新的代码:https://github.com/FrankFang/dom-2-prototype/blob/master/src/jquery.js

jQuery设计思想 阮一峰 2011

A guide to the basics of jQuery

JQ官网

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名