Skip to content

3-4filter方法及综合案例 ​

本页对应 第三章JavaScript进阶,共 7 段视频,约 55 分钟。

学习目标 ​

  1. 能使用 map 与 join 将对象数组渲染为商品列表,并通过解构提取 name、price、picture 等字段填充页面结构。
  2. 能封装带参数的渲染函数,传入不同商品数组后重新渲染页面,并说明参数化渲染对后续筛选业务的作用。
  3. 能使用 filter 按价格区间筛选数组,正确编写返回布尔值的回调条件,并说明其返回新数组且不改变原数组。
  4. 能使用事件委托为筛选区域注册点击事件,判断点击目标是否为 a 链接,并通过 dataset.index 获取当前链接序号。
  5. 能根据链接序号使用 filter 筛选商品并调用渲染函数更新页面,同时处理“全部区间”无需筛选的渲染逻辑。
  6. 能说明引用计数法与标记清除法的基本流程,并解释循环引用导致引用计数法无法回收、标记清除法如何解决该问题。

知识点 ​

先明确本节目标 ​

这一节要解决的是:理解面向对象的基础概念,能够利用构造函数创建对象。你先要会“造一个对象”,再会“按模板批量造对象”,最后能分清实例成员、静态成员,并理解为什么字符串、数字也能调用方法。

创建对象三种方式 ​

为什么需要:只创建一个对象时,字面量最直观;但对象多了,重复写结构会很啰嗦,所以还要了解 new Object 和构造函数。

怎么想:先掌握最常用的字面量,再把 new Object 当作了解内容,最后用构造函数解决“批量创建相似对象”。

怎么做:

html
<body>
  <script>
    // 1. 利用字面量创建对象(常用的一种方式)
    const o = {
      name: '佩奇'
    }
    console.log(o)

    // 2. 利用 new Object 创建对象 (了解)
    // const oo = new Object({ name: '佩奇' })
    const oo = new Object()
    oo.name = '佩奇'
    console.log(oo)

    // 3. 利用构造函数创建对象

  </script>

易错点:字面量适合单个对象;new Object() 后要逐个挂属性;不要误以为第三种方式已经写完,它需要下面构造函数配合。

构造函数 ​

为什么需要:创建佩奇后,再创建乔治如果还手写一遍 {...},结构重复。构造函数就是“对象模板”。

怎么想:把相同属性名写进函数,把不同属性值做成形参;用 this.属性 = 形参 初始化。约定首字母大写,并用 new 调用。

怎么做:

html
<body>
  <script>
    // 构造函数:是一种特殊的函数,用来创建对象(并完成初始化对象)
    // 1. 构造函数的2个约定 首字母要大写, 利用new关键字来调用函数
    // function Pig() {

    // }
    // console.log(new Pig)
    // console.log(Pig())
    function Pig(name, age, gender) {
      // this.name 属性   
      // name 是形参也就是属性值
      this.name = name
      this.age = age
      this.gender = gender
      // return 123
    }
    const peiqi = new Pig('佩奇', 6, '女')
    console.log(peiqi)
    const qiaozhi = new Pig('乔治', 3, '男')
    console.log(qiaozhi)

    // 2. 构造函数创建对象说明
    // 2.1 new 关键字 实例化对象
    // 2.2 如果构造函数没有参数,则可以省略小括号
    // 2.3 构造函数里面无需写 return
    // 2.4 new Object()  new Date() 也是在实例化对象
  </script>
</body>

易错点:不写 new 直接 Pig(),this 不会指向新对象;构造函数内部不用写 return 返回新对象;无参数时可省略 ();new Object()、new Date() 也是实例化。

new 实例化过程 ​

为什么需要:知道 new 做了什么,才能理解 this 为什么能挂到新对象上。

怎么想:按四步记:创建新空对象 → 构造函数 this 指向新对象 → 执行构造函数代码 → 返回新对象。

怎么做:上面 new Pig('佩奇', 6, '女') 执行时,先产生空对象,再让 this.name = name 等语句把属性挂上去,最后返回给 peiqi。

易错点:不要把 new 当成普通函数调用;构造函数里若返回基本类型,不会替换新对象;理解 this 指向是这一节的关键。

实例成员 & 静态成员 ​

为什么需要:对象上的属性和构造函数本身的属性不是一回事,混用会访问不到。

怎么想:通过构造函数创建出来的对象叫实例对象,它身上的属性和方法叫实例成员;直接挂在构造函数上的叫静态成员。

怎么做:

html
<body>
  <script>
    // 实例成员和静态成员
    // 1. 实例成员:实例对象上的属性和方法属于实例成员
    function Pig(name) {
      this.name = name
    }
    const peiqi = new Pig('佩奇')
    const qiaozhi = new Pig('乔治')
    peiqi.name = '小猪佩奇'  // 实例属性
    peiqi.sayHi = () => {  // 实例方法
      console.log('hi~~')
    }
    console.log(peiqi)
    console.log(qiaozhi)
    console.log(peiqi === qiaozhi)
  </script>
html
<script>
  // 2. 静态成员 : 构造函数上的属性和方法称为静态成员
  function Pig(name) {
    this.name = name
  }
  Pig.eyes = 2  // 静态属性
  Pig.sayHi = function () {  // 静态方法
    console.log(this)
  }
  Pig.sayHi()
  console.log(Pig.eyes)  // 2
</script>

易错点:实例对象彼此独立,peiqi.name 改了不影响 qiaozhi;静态成员只能通过构造函数访问,如 Pig.eyes,不要写成 peiqi.eyes;静态方法里的 this 指向构造函数。Date.now()、Math.PI、Math.random() 都是静态成员思路。

一切皆对象与包装类型 ​

为什么需要:字符串、数字、布尔是基本类型,但 'andy'.length 却能调用,这背后是包装类型。

怎么想:基本类型也有专门构造函数。执行 'andy'.length 时,会临时创建 String 实例,调用方法,再销毁实例。

怎么做:

html
<body>
  <script>
    // 包装类型
    // const str = 'andy'
    // console.log(str.length)

    // 包装过程
    // const str = new String('andy')
    // str.substring()
    // str = null 
  </script>
</body>

易错点:包装是临时过程,不要日常用 new String() 造字符串;null、undefined 没有对应包装类型,不能随意调方法。

延伸:对象数组与解构 ​

转写稿里的商品渲染案例可以帮你理解对象的实际使用:数据是对象数组,每个对象有 name、price、picture。渲染时用 map 加 join 拼字符串,再把对象属性填进模板。转写稿里的“迈普”就是 map,“招你”就是 join,“一耳一”就是 ele。调试时如果价格变量名写错,页面会异常,改成 price 后恢复正常。为了少写 ele.name、ele.price,可以解构:const { name, price, picture } = ele,之后直接用变量名。易错点:解构变量名要和对象属性名一致,否则拿到 undefined。

Object:先读属性,再做拷贝 ​

为什么需要:普通对象经常要取出所有属性名、所有属性值,或者把一个对象的内容复制到另一个对象里。Object 的静态方法就是为这些操作准备的。

怎么想:Object.keys() 和 Object.values() 返回数组,方便后续遍历;Object.assign(目标对象, 源对象) 把源对象属性复制到目标对象。静态方法只有构造函数 Object 能调用。

怎么做:

html
<body>
  <script>
    // Object 三种静态方法
    const o = { name: '佩奇', age: 6, gender: '女' }

    // 1. Object.keys()  得到对象所有属性(重点) 返回的是数组
    const key = Object.keys(o)
    console.log(key)

    // 2. Object.values()  得到对象所有属性值(重点)返回的是数组
    const value = Object.values(o)
    console.log(value)

    // 3. Object.assign(目标对象, 源对象)  对象的拷贝   assign 赋值的意思
    const oo = {}
    Object.assign(oo, o)
    oo.name = '小猪佩奇'
    console.log(oo)
    console.log(o)
    // 3.1 注意 拷贝对象之后是两个不同的对象,不会相互影响
  </script>
</body>

调试时可先打印 key、value,确认它们都是数组;再改 oo.name,观察 o.name 不变。转写稿里的对象数组渲染案例中,也常用解构把 ele.name、ele.price 直接取出来,减少重复写 ele.。

易错点:推荐用字面量 {} 声明对象,不推荐 new Object();Object.assign 示例演示的是顶层属性拷贝,改 oo.name 不影响 o.name;不要把 Object.keys 写成实例方法。

Array:用 reduce 做累计,用 filter 做筛选 ​

为什么需要:数组是高频数据容器。除了字面量创建,还要掌握累计、筛选、遍历等实例方法。本节重点看 reduce,转写稿还补充了 filter 的推导。

怎么想:arr.reduce(function(上一次值, 当前值){}, 初始值)。没有初始值时,第一次的“上一次值”是数组第一项;有初始值时,第一次的“上一次值”就是初始值,当前值从第一项开始。

怎么做:

html
<body>
  <script>
    // 数组reduce方法
    // arr.reduce(function(上一次值, 当前值){}, 初始值)
    // const arr = [1, 5, 8]

    // 1. 没有初始值 
    // const total = arr.reduce(function (prev, current) {
    //   return prev + current
    // })
    // console.log(total)

    // 上一次值    当前值    返回值  (第一次循环)
    //   1         5         6
    // 上一次值    当前值    返回值  (第二次循环)
    //   6         8        14

    const arr = [1, 5, 8]

    // 2. 有初始值
    const total = arr.reduce(function (prev, current) {
      return prev + current
    }, 10)
    console.log(total)

    // 上一次值    当前值    返回值  (第一次循环)
    //   10         1         11
    // 上一次值    当前值    返回值  (第二次循环)
    //   11         5         16
    // 上一次值    当前值    返回值  (第三次循环)
    //   16         8         24

  </script>
</body>

转写稿中 filter 的调试过程是:数组 [10,20,30,40],return ele >= 30,10、20 不满足被跳过,30、40 满足进入新数组。若写成 return ele + 30,因为结果都是真值,所有元素都会被保留,所以 filter 的 return 应写判断条件,而不是像 map 那样做加工。

易错点:推荐字面量声明数组;reduce 要分清有没有初始值;filter 返回新数组,不改变原数组;filter 的 return 是筛选条件,结果为真才保留。

String:拆分、反转、拼接 ​

为什么需要:字符串经常要按分隔符拆成数组,或把数组拼回字符串。做字符串翻转时,字符串本身没有数组的 reverse,需要借助数组。

怎么想:split('') 把字符逐个拆开,reverse() 反转数组,join('') 再拼回字符串。转写稿中的“传智播客”翻转就是按这个思路调试出来的。

怎么做:

html
<body>
  <script>
    // 字符串方法split 
    const str = '传智播客'
    // 1. split('分隔符')  把字符串转换为数组
    console.log(str.split(''))
    const str1 = '小米,华为,苹果'
    console.log(str1.split(','))

    // 2. join('分隔符')可以把数组转换为字符串

    // 3. 把传智播客这字符串做一个翻转 变成 客播智传

    // 把字符串转换为数组, 数组里面reverse翻转,再把数组转换为字符串
    console.log(str.split('').reverse())
    console.log(str.split('').reverse().join(''))
  </script>
</body>

易错点:split('') 的空字符串表示按字符拆;split(',') 要写对分隔符;reverse() 是数组方法,不能直接 str.reverse();join('') 不写空字符串可能拼出逗号。另注意 String 也可当普通函数使用,用于强制转换成字符串。

Number:用 toFixed 控制小数位 ​

为什么需要:价格、统计结果常要显示固定小数位。toFixed 可以把数值格式化成指定小数位。

怎么想:num.toFixed(n) 保留 n 位小数,整数也会补 .00。看示例:12.345.toFixed(2) 得到 12.35,12.345.toFixed(1) 得到 12.3,12.toFixed(2) 得到 12.00。

怎么做:

html
<body>
  <script>
    // 数字 toFixed 方法
    const num = 12.345
    console.log(num.toFixed(2))  // 12.35
    console.log(num.toFixed(1))  // 12.3
    const num1 = 12
    console.log(num1.toFixed(2))  // 12.00
  </script>
</body>

易错点:推荐字面量声明数值,不推荐 new Number();toFixed 返回的是字符串,不是数字;保留位数要按显示需求传参,不要和数学计算混在一起。

课件图示 ​

课件第23页:多一句没有,少一句不行,用最短时间,教会最实用的技术! 2.1 Object O

课件第32页:多一句没有,少一句不行,用最短时间,教会最实用的技术! 2.2 Array Ar

课件第5页:多一句没有,少一句不行,用最短时间,教会最实用的技术! 1.1创建对象三种方式(

课件第7页:多一句没有,少一句不行,用最短时间,教会最实用的技术! 1.2 构造函数 l 构

课件第8页:多一句没有,少一句不行,用最短时间,教会最实用的技术! 1.2 构造函数 构造函

课件第9页:多一句没有,少一句不行,用最短时间,教会最实用的技术! 1.2 构造函数 l 构

课件第10页:多一句没有,少一句不行,用最短时间,教会最实用的技术! 总结 总结 1. 构造函

课件第12页:多一句没有,少一句不行,用最短时间,教会最实用的技术! 1.2 构造函数 l n

代码示例 ​

示例一:构造函数 + 实例成员 / 静态成员 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>构造函数</title>
</head>
<body>
  <script>
    // 构造函数:是一种特殊的函数,用来创建对象(并完成初始化)
    // 两个约定:首字母大写、配合 new 关键字调用
    function Pig(name, age, gender) {
      // this.name 是属性,name 是形参(也就是属性值)
      this.name = name
      this.age = age
      this.gender = gender
      // return 123   // 构造函数内部无需写 return
    }

    const peiqi = new Pig('佩奇', 6, '女')   // new 关键字 实例化对象
    const qiaozhi = new Pig('乔治', 3, '男')
    console.log(peiqi)
    console.log(qiaozhi)

    // 1. 实例成员:实例对象上的属性和方法
    peiqi.name = '小猪佩奇'          // 实例属性
    peiqi.sayHi = () => {            // 实例方法
      console.log('hi~~')
    }
    peiqi.sayHi()
    console.log(peiqi)
    console.log(qiaozhi)             // 乔治不受影响
    console.log(peiqi === qiaozhi)   // false

    // 2. 静态成员:构造函数上的属性和方法
    Pig.eyes = 2                     // 静态属性
    Pig.sayHi = function () {        // 静态方法
      console.log(this)              // this 指向 Pig 构造函数
    }
    Pig.sayHi()
    console.log(Pig.eyes)            // 2
    console.log(peiqi.eyes)          // undefined,实例访问不到静态成员
  </script>
</body>
</html>

要点解释 / 易错点:

  • new Pig(...) 的实例化过程:① 创建新空对象 ② 构造函数 this 指向新对象 ③ 执行构造函数代码 ④ 返回新对象。
  • 构造函数内部无需写 return;即使写了 return 123,返回值仍是新创建的对象。
  • 直接写 Pig() 调用,this 指向 window,得到 undefined——必须用 new。
  • 实例化时如果没有参数,可以省略小括号,如 new Pig;new Object()、new Date() 同样是实例化。
  • 实例成员彼此独立互不影响:改 peiqi.name 不会影响 qiaozhi。
  • 静态成员只能由构造函数访问(Pig.eyes),实例访问是 undefined;静态方法里的 this 指向构造函数。

示例二:渲染商品列表(map + join + 解构) ​

js
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>渲染商品列表</title>
  <style>
    .list { display: flex; flex-wrap: wrap; }
    .item { width: 200px; margin: 10px; border: 1px solid #ccc; }
    .item img { width: 100%; }
  </style>
</head>
<body>
  <div class="list"></div>
  <script>
    // 典型的对象数组
    const goodsList = [
      { id: 1, name: '小米', price: 100, picture: 'https://picsum.photos/id/1/200/200' },
      { id: 2, name: '华为', price: 200, picture: 'https://picsum.photos/id/2/200/200' },
      { id: 3, name: '苹果', price: 488, picture: 'https://picsum.photos/id/3/200/200' },
      { id: 4, name: '荣耀', price: 300, picture: 'https://picsum.photos/id/4/200/200' },
      { id: 5, name: '红米', price: 50,  picture: 'https://picsum.photos/id/5/200/200' },
      { id: 6, name: 'vivo', price: 350, picture: 'https://picsum.photos/id/6/200/200' }
    ]

    // map 遍历数组,返回一个新数组(数组里装的是 DIV 字符串)
    const strArr = goodsList.map(ele => {
      // 解构:不用再写 ele.xxx
      const { name, price, picture } = ele
      return `
        <div class="item">
          <img src="${picture}" alt="">
          <h4>${name}</h4>
          <span>${price}元</span>
        </div>`
    })

    // join('') 把数组转成纯字符串,再追加到父盒子
    document.querySelector('.list').innerHTML = strArr.join('')
  </script>
</body>
</html>

要点解释 / 易错点:

  • 渲染思路:数组 map 生成字符串数组 → join('') 转字符串 → 赋值给父盒子 innerHTML。
  • join('') 的括号里必须写空字符串,否则默认用逗号连接,页面上会多出逗号。
  • 解构 const { name, price, picture } = ele 中,变量名必须和对象属性名一致,否则拿到 undefined。
  • 箭头函数只有一个参数时小括号可省略:ele => {}。
  • 模板字符串取变量必须写 ${},漏写 $ 只会输出字面文字。
  • innerHTML 赋的必须是字符串,直接把数组赋进去会被隐式转成逗号拼接的字符串。

示例三:综合案例——价格筛选 + 重新渲染 ​

js
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>价格筛选</title>
  <style>
    .filter a { display: inline-block; padding: 5px 10px; margin-right: 10px;
                background: #eee; color: #333; text-decoration: none; }
    .filter a:hover, .filter a:active { background: #27ba9b; color: #fff; }
    .list { display: flex; flex-wrap: wrap; }
    .item { width: 200px; margin: 10px; border: 1px solid #ccc; }
    .item img { width: 100%; }
  </style>
</head>
<body>
  <div class="filter">
    <a href="javascript:;" data-index="1">0-100元</a>
    <a href="javascript:;" data-index="2">100-300元</a>
    <a href="javascript:;" data-index="3">300元以上</a>
    <a href="javascript:;">全部区间</a>
  </div>
  <div class="list"></div>

  <script>
    const goodsList = [
      { id: 1, name: '小米', price: 100, picture: 'https://picsum.photos/id/1/200/200' },
      { id: 2, name: '华为', price: 200, picture: 'https://picsum.photos/id/2/200/200' },
      { id: 3, name: '苹果', price: 488, picture: 'https://picsum.photos/id/3/200/200' },
      { id: 4, name: '荣耀', price: 300, picture: 'https://picsum.photos/id/4/200/200' },
      { id: 5, name: '红米', price: 50,  picture: 'https://picsum.photos/id/5/200/200' },
      { id: 6, name: 'vivo', price: 350, picture: 'https://picsum.photos/id/6/200/200' }
    ]

    // 1. 渲染业务:封装函数,参数 arr 决定渲染哪批数据
    function render(arr) {
      const strArr = arr.map(ele => {
        const { name, price, picture } = ele
        return `
          <div class="item">
            <img src="${picture}" alt="">
            <h4>${name}</h4>
            <span>${price}元</span>
          </div>`
      })
      document.querySelector('.list').innerHTML = strArr.join('')
    }

    // 页面一打开先渲染全部商品
    render(goodsList)

    // 2. 筛选业务:事件委托给 .filter,不要给每个 a 单独加
    document.querySelector('.filter').addEventListener('click', function (e) {
      const { tagName, dataset } = e.target
      if (tagName === 'A') {
        // 初始值给全部数据,解决“全部区间”的问题
        let arr = goodsList

        if (dataset.index === '1') {
          arr = goodsList.filter(item => item.price >= 0 && item.price <= 100)
        } else if (dataset.index === '2') {
          arr = goodsList.filter(item => item.price >= 100 && item.price <= 300)
        } else if (dataset.index === '3') {
          arr = goodsList.filter(item => item.price >= 300)
        }

        // 用筛选后的新数组重新渲染页面
        render(arr)
      }
    })
  </script>
</body>
</html>

要点解释 / 易错点:

  • render 带一个参数 arr:传不同数据进去,页面就显示不同商品,这是能复用的关键。
  • 事件委托绑定在 .filter 上,通过 e.target 判断点的是不是 A;tagName 拿到的是大写的 'A',写成 'a' 判断永远不成立。
  • dataset.index 拿到的是字符串 '1',比较时必须加引号,写 === 1 永远为 false。
  • 解构 const { tagName, dataset } = e.target 后就不用反复写 e.target.xxx。
  • let arr = goodsList 必须写在 if 外面,且用 let:点“全部区间”时下面的 if 都不执行,arr 仍是全部数据,直接渲染即可;用 const 重新赋值会报错。
  • filter 回调 return 的是筛选条件(真假值),为真就把该元素放进新数组;不要像 map 那样返回处理后的值,写 item.price + 30 会因为结果恒为真而把所有元素都留下。
  • 边界值:>= 与 <= 让 100 元的商品同时出现在“0-100元”和“100-300元”,300 元的商品同时出现在“100-300元”和“300元以上”,这是按笔记中的条件刻意保留的。

常见错误 ​

  1. Pig('佩奇') 返回 undefined 或污染全局 → 构造函数未用 new 调用,this 未指向新实例 → 统一使用 new Pig(...),并保持构造函数首字母大写。
  2. 构造函数内写 return {...} 后实例字段异常 → 误以为要手动返回新对象 → 删除多余 return,让 new 自动返回新实例。
  3. Pig.eyes 在实例上取不到,peiqi.sayHi 在构造函数上取不到 → 实例成员与静态成员挂载位置混淆 → 实例成员挂 this/实例,静态成员挂构造函数,按访问主体调用。
  4. Object.assign({}, o) 后修改嵌套对象,原对象也变化 → 把 assign 当深拷贝 → 明确其为浅拷贝;嵌套结构需递归拷贝、结构化克隆或第三方库。
  5. filter 中写 return ele + 30 得到原数组全量 → filter 按返回值真值筛选,不是映射 → 条件返回布尔表达式;需要映射改用 map。
  6. map 渲染后直接 list.innerHTML = arr 出现逗号或异常文本 → 数组被隐式转字符串 → 先 arr.join('') 再赋值。
  7. 点击“全部区间”页面空白 → data-index 是字符串且筛选数组未默认完整数据 → 比较用 '1' 或转数值;渲染数组先默认完整数据,if 内覆盖,render 放 if 后。
  8. 事件委托中点到 A 内元素取不到序号 → e.target 是最深点击元素 → 用 closest('a') 或校验 tagName 后再取 dataset.index。

练习 ​

  1. 用构造函数 Book(title, price) 创建两个实例;修改其中一个实例的 title,验证另一个不受影响;再给构造函数添加静态属性和静态方法并访问。
  2. 给定对象数组 goods,用 map + join 渲染商品列表,要求解构 name、price、picture 后填充到 .item 结构,并追加到 .list。
  3. 给定数组 [10, 20, 30, 40],用 filter 得到 >= 30 的新数组;再改成 return ele + 30,观察并解释结果差异。
  4. 给 .filter 注册事件委托:仅点击 A 时读取 data-index;根据 1/2/3 分别筛选 0–100、100–300、300+ 的商品并调用 render(arr);点击“全部区间”恢复完整商品列表。

原视频对照 ​

#视频时长
1渲染页面案例08:13
2综合案例-渲染业务制作07:49
3filter筛选数组使用10:01
4综合案例-注册点击事件并得到链接序号05:11
5综合案例-根据序号渲染页面10:02
6拓展-垃圾回收机制-引用计数法10:53
7拓展-垃圾回收机制标记清除法03:15

小结 ​

  1. 构造函数必须用 new 调用,实例彼此独立;静态成员只能通过构造函数访问。
  2. new 实例化过程决定 this 指向新对象,构造函数通常无需写 return。
  3. 列表渲染优先使用 map → join → innerHTML,填充数据前解构可减少重复前缀。
  4. filter 的 return 是筛选条件,真值保留、假值丢弃;映射场景用 map。
  5. 事件委托绑定父容器,注意 target 是最深点击元素,data-index 取到的是字符串。
  6. 筛选与渲染分离:渲染函数接收数组参数,筛选数组默认完整数据,才能兼容“全部区间”。
  7. Object.assign 是浅拷贝;引用计数遇到循环引用无法回收,标记清除按根可达性判断。