主题
3-4filter方法及综合案例
本页对应 第三章JavaScript进阶,共 7 段视频,约 55 分钟。
学习目标
- 能使用
map与join将对象数组渲染为商品列表,并通过解构提取name、price、picture等字段填充页面结构。 - 能封装带参数的渲染函数,传入不同商品数组后重新渲染页面,并说明参数化渲染对后续筛选业务的作用。
- 能使用
filter按价格区间筛选数组,正确编写返回布尔值的回调条件,并说明其返回新数组且不改变原数组。 - 能使用事件委托为筛选区域注册点击事件,判断点击目标是否为
a链接,并通过dataset.index获取当前链接序号。 - 能根据链接序号使用
filter筛选商品并调用渲染函数更新页面,同时处理“全部区间”无需筛选的渲染逻辑。 - 能说明引用计数法与标记清除法的基本流程,并解释循环引用导致引用计数法无法回收、标记清除法如何解决该问题。
知识点
先明确本节目标
这一节要解决的是:理解面向对象的基础概念,能够利用构造函数创建对象。你先要会“造一个对象”,再会“按模板批量造对象”,最后能分清实例成员、静态成员,并理解为什么字符串、数字也能调用方法。
创建对象三种方式
为什么需要:只创建一个对象时,字面量最直观;但对象多了,重复写结构会很啰嗦,所以还要了解 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 返回的是字符串,不是数字;保留位数要按显示需求传参,不要和数学计算混在一起。
课件图示








代码示例
示例一:构造函数 + 实例成员 / 静态成员
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元以上”,这是按笔记中的条件刻意保留的。
常见错误
Pig('佩奇')返回undefined或污染全局 → 构造函数未用new调用,this未指向新实例 → 统一使用new Pig(...),并保持构造函数首字母大写。- 构造函数内写
return {...}后实例字段异常 → 误以为要手动返回新对象 → 删除多余return,让new自动返回新实例。 Pig.eyes在实例上取不到,peiqi.sayHi在构造函数上取不到 → 实例成员与静态成员挂载位置混淆 → 实例成员挂this/实例,静态成员挂构造函数,按访问主体调用。Object.assign({}, o)后修改嵌套对象,原对象也变化 → 把assign当深拷贝 → 明确其为浅拷贝;嵌套结构需递归拷贝、结构化克隆或第三方库。filter中写return ele + 30得到原数组全量 →filter按返回值真值筛选,不是映射 → 条件返回布尔表达式;需要映射改用map。map渲染后直接list.innerHTML = arr出现逗号或异常文本 → 数组被隐式转字符串 → 先arr.join('')再赋值。- 点击“全部区间”页面空白 →
data-index是字符串且筛选数组未默认完整数据 → 比较用'1'或转数值;渲染数组先默认完整数据,if内覆盖,render放if后。 - 事件委托中点到
A内元素取不到序号 →e.target是最深点击元素 → 用closest('a')或校验tagName后再取dataset.index。
练习
- 用构造函数
Book(title, price)创建两个实例;修改其中一个实例的title,验证另一个不受影响;再给构造函数添加静态属性和静态方法并访问。 - 给定对象数组
goods,用map + join渲染商品列表,要求解构name、price、picture后填充到.item结构,并追加到.list。 - 给定数组
[10, 20, 30, 40],用filter得到>= 30的新数组;再改成return ele + 30,观察并解释结果差异。 - 给
.filter注册事件委托:仅点击A时读取data-index;根据1/2/3分别筛选 0–100、100–300、300+ 的商品并调用render(arr);点击“全部区间”恢复完整商品列表。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 渲染页面案例 | 08:13 |
| 2 | 综合案例-渲染业务制作 | 07:49 |
| 3 | filter筛选数组使用 | 10:01 |
| 4 | 综合案例-注册点击事件并得到链接序号 | 05:11 |
| 5 | 综合案例-根据序号渲染页面 | 10:02 |
| 6 | 拓展-垃圾回收机制-引用计数法 | 10:53 |
| 7 | 拓展-垃圾回收机制标记清除法 | 03:15 |
小结
- 构造函数必须用
new调用,实例彼此独立;静态成员只能通过构造函数访问。 new实例化过程决定this指向新对象,构造函数通常无需写return。- 列表渲染优先使用
map → join → innerHTML,填充数据前解构可减少重复前缀。 filter的return是筛选条件,真值保留、假值丢弃;映射场景用map。- 事件委托绑定父容器,注意
target是最深点击元素,data-index取到的是字符串。 - 筛选与渲染分离:渲染函数接收数组参数,筛选数组默认完整数据,才能兼容“全部区间”。
Object.assign是浅拷贝;引用计数遇到循环引用无法回收,标记清除按根可达性判断。