Skip to content

3-6数组方法进阶及案例 ​

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

学习目标 ​

  • 能写出 reduce 的语法,说明回调函数中上一次值、当前值与初始值的作用,并正确实现数组求和。
  • 能根据有无初始值推导 reduce 的执行过程,准确说明遍历次数、每次返回值及最终结果。
  • 能使用 reduce 对对象数组按指定属性(如 salary)累加,并正确设置初始值为 0。
  • 能区分 find 与 findIndex 的返回值,说明找不到时分别返回 undefined 和 -1,并与 filter 的返回值和查找方式作对比。
  • 能使用 every 和 some 检测数组元素是否满足条件,并解释二者在“全部满足”和“任一满足”场景下的布尔返回值差异。
  • 能使用 Array.from 将类数组转换为真数组,说明其静态方法调用方式、返回真数组且不修改原类数组。

知识点 ​

从“蛋炒饭”理解面向过程 ​

为什么需要:当问题步骤清晰、目标单一,尤其是贴近硬件或性能敏感的场景,先把流程拆成步骤最直接。面向过程不是“落后”,而是适合“按顺序把事情做完”。

怎么想:分析解决问题需要哪些步骤,把每一步封装成函数,使用时按顺序调用。就像蛋炒饭:准备食材、打蛋、炒蛋、下饭、调味、出锅,步骤之间靠数据传递衔接。

蛋炒饭示意图

怎么做:先列步骤,再写函数,最后在主流程中依次调用。转写稿里 reduce 求和的推导也是这种思路:没有初始值时,上一次值 prev 以数组第一个元素为准,[1,5,8] 先算 1+5=6,再算 6+8=14,三个元素只遍历两次;有初始值 10 时,第一次是 10+1=11,再 11+5=16,再 16+8=24,有几个元素就遍历几次。

易错点:不要把面向过程等同于“低级”;步骤之间若数据传递不清,函数会越写越乱;复用性较差,步骤一变就要改多处。

面向对象:把事务分解成对象分工 ​

为什么需要:大型软件项目需要多人合作,若所有逻辑都堆在步骤里,维护成本高。面向对象把事务分解成一个个对象,让对象成为功能中心,具有明确分工。

怎么想:先识别有哪些对象、每个对象负责什么、对象之间如何协作。对象不只是一堆数据,还包括操作数据的方法。

面向对象分工示意图

怎么做:在 JavaScript 中,本节学习目标是理解基于原型的面向对象编程语法实现,并理解封装、继承、多态三个特征。封装是隐藏细节、暴露接口;继承是复用共性;多态是同一接口有不同表现。

易错点:对象不是越多越好,职责不清会让系统更乱;继承不是把代码复制一遍;封装不等于随便加个函数;多态也不是简单重载。还要注意“编程”不要写成“编程序”。

编程思想对比:选择而非站队 ​

为什么需要:知道优缺点,才能按场景选方案,而不是把某一种思想绝对化。

怎么想:面向过程性能通常更高,适合跟硬件联系紧密的东西,例如单片机;缺点是灵活性和复用性较差。面向对象易维护、易复用、易扩展,借助封装、继承、多态可以设计低耦合系统;缺点是性能通常比面向过程低。

怎么做:小脚本、流程固定的逻辑可优先面向过程;多人协作、需求易变、对象关系明显的系统可优先面向对象。笔记也提醒:生活离不开蛋炒饭,也离不开盖浇饭,选择不同而已;只不过前端不同于其他语言,面向过程更多。

易错点:不要认为面向对象一定性能高;也不要认为面向过程不能复用。真正要避免的是“为了面向对象而面向对象”。

用转写稿的 reduce 调试过程看“步骤化”与“对象化” ​

为什么需要:转写稿中的数组方法调试,能帮助我们看清“步骤化执行”和“按对象属性处理”的差别。比如薪资案例里,数组元素不是数字而是对象,不能直接拿对象累加。

怎么想:reduce 的回调函数第一个参数是上一次返回值 prev,第二个是当前元素 current;初始值会影响结果。对象数组必须给初始值 0,并累加 current.salary,否则第一次 prev 会变成第一个对象,无法相加。转写稿中“李四1500”与后续“1万+1500=25000”矛盾,按推导应纠正为 15000。

怎么做:

js
const total = [
  { name: '张三', salary: 10000 },
  { name: '李四', salary: 15000 },
  { name: '王五', salary: 20000 }
].reduce((prev, current) => prev + current.salary, 0);

console.log(total); // 45000

调试时先打印 prev、current,确认第一次是 0 和第一个对象,第二次是 10000 和第二个对象,最后得到 45000。同音字也要纠正:伪数组不是“尾数组”,NodeList 不是“not list”,push 不是“po”,findIndex 返回索引,找不到返回 -1;every 要求每个都满足,some 只要一个满足;Array.from 可把伪数组转成真数组,且不修改原伪数组。

易错点:省略初始值导致对象累加失败;把 find 返回元素和 findIndex 返回索引混淆;把 filter 返回数组和 find 返回元素混淆;忘记 return。掌握这些调试过程后,再回到面向对象学习,就更容易理解“对象有属性、有方法、按职责协作”的意义。

先明确:本节要解决“封装”问题 ​

为什么需要:面向对象思想里,封装很重要。若每个对象都单独写姓名、年龄和 sayHi,重复代码多,也不好维护。
怎么想:把公共属性和方法抽取出来,封装到一个“模板”里,再用这个模板创建具体对象。这个模板就是构造函数。
怎么做:用 function Person(name, age) { ... } 定义构造函数,在内部用 this.name、this.age 保存属性,用 this.sayHi = function () { ... } 添加方法。
易错点:构造函数是“用来创建实例的模板”,不要把它和普通函数调用混为一谈;本节示例统一用 new Person(...) 创建实例。

用构造函数创建实例对象 ​

为什么需要:有了构造函数,创建多个同类对象时只需传入不同数据。
怎么想:new Person('张三', 18) 会创建一个实例,并把传入的 name、age 放到这个实例上。
怎么做:保留笔记代码:

html
<body>
  <script>
    // 1. 构造函数实现封装,封装人的姓名、年龄和 sayHi方法

    function Person(name, age) {
      this.name = name
      this.age = age
      this.sayHi = function () {
        console.log('hi~')
      }
    }

    // 实例化
    const zs = new Person('张三', 18)
    const ls = new Person('李四', 19)
    console.log(zs)
    console.log(ls)
    console.log(zs === ls)  // false 

    // 2. 构造函数实现封装有个小问题
    console.log(zs.sayHi === ls.sayHi)  // false 两个函数不一样

  </script>
</body>

易错点:zs 和 ls 虽然都由 Person 创建,但它们是两个不同实例,所以 zs === ls 为 false。

实例对象彼此独立、互不影响 ​

为什么需要:封装后,每个实例应保存自己的数据,不能因为创建了 ls 就改掉 zs 的信息。
怎么想:zs 的 name 是“张三”,ls 的 name 是“李四”,它们各自保存自己的属性值。
怎么做:通过 console.log(zs)、console.log(ls) 观察两个实例;再用 console.log(zs === ls) 验证它们不是同一个对象。
易错点:不要因为两个对象“长得像”就认为相等。对象比较看是否同一引用,本例输出为 false,说明它们彼此独立。

同名方法不等于同一个函数 ​

为什么需要:理解 zs.sayHi === ls.sayHi 为什么是 false,才能发现构造函数写法里的内存问题。
怎么想:每次 new Person(...) 都会执行一次 this.sayHi = function () { ... },相当于为每个实例各创建一个函数对象。
怎么做:代码里用 console.log(zs.sayHi === ls.sayHi) 比较,注释已给出结果 false,即“两个函数不一样”。
易错点:方法名都叫 sayHi,不代表它们是同一个函数。本节总结也指出:构造函数实例创建的对象彼此独立、互不影响;但前面学过的构造函数方法很好用,却存在“浪费内存”的问题。本节先把这个现象暴露出来。

调试过程与转写稿取舍 ​

为什么需要:调试时要把“实例独立”和“方法不共享”分开验证。
怎么想:先看对象是否相等,再看方法是否相等;两个比较都输出 false,分别说明实例不同、函数对象也不同。
怎么做:按代码顺序运行,观察 console.log(zs)、console.log(ls)、zs === ls、zs.sayHi === ls.sayHi 四处输出。转写稿中有“通过构造函数 new 出来的实例对象身上有的一些方法”这一表述,可借它理解 zs.sayHi、ls.sayHi 是挂在各自实例上的方法,因此比较结果为 false。
易错点:转写稿大量内容在讲数组 reduce、find、every、Array.from 等内置构造函数方法,和本节“自定义构造函数实现封装”不是同一主题,不能硬套。转写稿中的同音字应按语义纠正,如“勾造函数”应为“构造函数”,“实力对象”应为“实例对象”,“返馈”应为“返回”,“初始证”应为“初始值”,“缩引号”应为“索引号”。本节只采用与构造函数、实例对象相关的表述,不编造数组内容。

总结:构造函数体现了面向对象的封装特性;实例对象彼此独立、互不影响;同时要记住示例暴露的问题——每个实例各自创建方法,zs.sayHi === ls.sayHi 为 false,存在浪费内存的问题。

说明与范围 ​

提供的转写稿讲的是数组 reduce/find/every/Array.from 等内容,与本节“原型”笔记不匹配;为避免编造,下面只依据本节笔记扩写原型知识,不把数组内容写入原型推导。转写稿中的同音字问题(如“勾造函数/返馈”等)因内容无关,未用于正文。

原型对象-prototype ​

为什么需要:构造函数封装方法时,若把函数写在构造函数内部,每次 new 都会创建一份函数,浪费内存。
怎么想:把公共方法挂到构造函数的 prototype 上,让所有实例共享。
怎么做:Person.prototype.sayHi = function () { ... },实例调用 zs.sayHi()、ls.sayHi()。
易错点:原型对象里的函数若要用 this,不要用箭头函数;箭头函数没有 this,也不能做构造函数。

html
<body>
  <script>
    function Person(name, age) {
      this.name = name
      this.age = age
    }
    Person.prototype.sayHi = function () {
      console.log('hi~')
    }
    console.log(Person.prototype)
    const zs = new Person('张三', 18)
    const ls = new Person('李四', 19)
    zs.sayHi()
    ls.sayHi()
    console.log(zs.sayHi === ls.sayHi) // true
  </script>
</body>

constructor 属性 ​

为什么需要:用对象形式重写 prototype 会覆盖原原型内容,constructor 可能不再指向原构造函数。
怎么想:constructor 在原型对象里,默认指向该原型对象的构造函数。
怎么做:重写原型时手动写 constructor: Person。
易错点:不修正 constructor 会丢失指向;不要把它和实例的 __proto__ 混淆。

html
<body>
  <script>
    function Person(name) {
      this.name = name
    }
    console.log(Person.prototype.constructor === Person) // true

    Person.prototype = {
      constructor: Person,
      sing() {
        console.log('我会唱歌')
      },
      dance() {
        console.log('我会跳舞')
      }
    }
    console.log(Person.prototype)
  </script>
</body>

原型 proto ​

为什么需要:实例对象要能使用构造函数原型对象上的方法。
怎么想:对象有 __proto__,指向构造函数的 prototype 原型对象。
怎么做:zs.__proto__ === Person.prototype 为 true。
易错点:prototype 是原型对象,__proto__ 是原型;__proto__ 现代浏览器显示为 [[Prototype]],意义相同;尽量不要修改它,否则影响性能。

html
<body>
  <script>
    function Person(name) {
      this.name = name
    }
    const zs = new Person('张三')
    console.log(zs)
    console.log(zs.__proto__ === Person.prototype) // true
  </script>
</body>

原型链 ​

为什么需要:访问对象成员时需要明确的查找路线。
怎么想:先找对象自身,再找它的原型对象,再找原型对象的原型,直到 Object 为止(null)。
怎么做:下面示例中 zs.sayHi 会按实例、Person.prototype、Object.prototype 的顺序查找。
易错点:找不到成员返回 undefined;不要随意改 __proto__,会影响性能。

原型链示意图

html
<body>
  <script>
    function Person(name) {
      this.name = name
    }
    const zs = new Person('张三')
    zs.sayHi = function () {
      console.log('实例对象的方法')
    }
    Person.prototype.sayHi = function () {
      console.log('Person原型对象的方法')
    }
    Object.prototype.sayHi = function () {
      console.log('Object原型对象的方法')
    }
    zs.sayHi()
  </script>
</body>

instanceof 运算符 ​

为什么需要:检测实例对象与构造函数之间的原型链关系。
怎么想:看构造函数 prototype 是否存在于实例对象的原型链上。
怎么做:zs instanceof Person、arr instanceof Array、arr instanceof Object。
易错点:instanceof 检测的是原型链,不是只看构造函数名字;数组既是 Array 实例,也在 Object 原型链上。

instanceof与原型链示意图

html
<body>
  <script>
    function Person(name) {
      this.name = name
    }
    function Person1(name) {
      this.name = name
    }
    const zs = new Person('张三')
    console.log(zs instanceof Person)  // true
    console.log(zs instanceof Person1) // false

    const arr = [1, 2, 3]
    console.log(arr instanceof Array)  // true
    console.log(arr instanceof Object) // true
    console.log(arr.__proto__ === Array.prototype) // true
    console.log(Array.prototype.__proto__ === Object.prototype) // true
  </script>
</body>

原型继承 ​

为什么需要:公共属性和方法可写到父级,子级通过继承复用。
怎么想:JavaScript 大多借助原型对象实现继承;子构造函数原型指向父构造函数实例,并修正 constructor。
怎么做:Man.prototype = new Person(),再 Man.prototype.constructor = Man;子级独有方法挂到自己的原型上。
易错点:继承后要修正 constructor;子级独有方法应挂在子级原型上,避免影响其他子级;共享父实例属性时要注意相互影响。

html
<body>
  <script>
    function Person() {
      this.eyes = 2
    }
    Person.prototype.eat = function () {
      console.log('我会吃饭')
    }

    function Man() {}
    function Woman() {}

    Man.prototype = new Person()
    Man.prototype.constructor = Man
    const zs = new Man()

    Woman.prototype = new Person()
    Woman.prototype.constructor = Woman
    const xl = new Woman()

    Woman.prototype.baby = function () {
      console.log('我会生孩子')
    }
    console.log(xl)
    console.log(zs)
  </script>
</body>

小结 ​

原型解决构造函数方法重复创建的问题;constructor 维护原型与构造函数的指向;__proto__ 连接实例与原型;原型链规定成员查找路线;instanceof 检测原型链关系;原型继承实现公共属性和方法复用。

课件图示 ​

课件第18页:多一句没有,少一句不行,用最短时间,教会最实用的技术! 3.1 原型对象 是什么

课件第19页:多一句没有,少一句不行,用最短时间,教会最实用的技术! 3.1 原型对象 实例对

课件第20页:多一句没有,少一句不行,用最短时间,教会最实用的技术! 总结 总结 1. 原型对

课件第21页:多一句没有,少一句不行,用最短时间,教会最实用的技术! 3.1 原型对象- th

课件第26页:多一句没有,少一句不行,用最短时间,教会最实用的技术! 3.2 construc

课件第30页:多一句没有,少一句不行,用最短时间,教会最实用的技术! 3.3 原型 对象都会有

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

课件第33页:多一句没有,少一句不行,用最短时间,教会最实用的技术! 练习 根据下面代码,请画

代码示例 ​

示例一:构造函数 + 原型(封装与原型链) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>构造函数与原型</title>
</head>
<body>
  <script>
    // 1. 构造函数实现封装:封装姓名、年龄、sayHi 方法
    function Person(name, age) {
      this.name = name
      this.age = age
      // this.sayHi = function () { console.log('hi~') }
      // 上面写法每 new 一次就创建一个新函数,浪费内存
    }

    // 2. 把方法挂到原型对象上,所有实例共享同一个函数
    Person.prototype.sayHi = function () {
      console.log('hi~', this.name)   // 原型对象里的 this 也指向实例对象
    }

    const zs = new Person('张三', 18)
    const ls = new Person('李四', 19)

    zs.sayHi()
    ls.sayHi()

    console.log(zs === ls)                                // false
    console.log(zs.sayHi === ls.sayHi)                    // true(共享同一个函数)
    console.log(Person.prototype.constructor === Person)  // true
    console.log(zs.__proto__ === Person.prototype)        // true

    // 3. 原型链查找:实例自身 -> Person.prototype -> Object.prototype -> null
    Object.prototype.sayHi = function () {
      console.log('Object原型对象的方法')
    }
    zs.sayHi()              // 就近原则,仍走 Person.prototype 上的方法

    delete Person.prototype.sayHi
    zs.sayHi()              // Person.prototype 上没有了,沿 __proto__ 找到 Object.prototype

    // 4. 用对象形式重写原型对象时,要手动把 constructor 指回来
    Person.prototype = {
      constructor: Person,   // 不写这句,constructor 就不再指向 Person
      sing() { console.log('我会唱歌') },
      dance() { console.log('我会跳舞') }
    }
    console.log(Person.prototype.constructor === Person)  // true
  </script>
</body>
</html>

关键行与易错点

  • Person.prototype.sayHi = function () {}:方法写在原型上,zs.sayHi === ls.sayHi 才是 true;写在构造函数里每次实例化都会新建函数,浪费内存。
  • 原型对象里的方法不要用箭头函数,箭头函数没有自己的 this,会拿不到实例对象;构造函数同理,箭头函数不能做构造函数。
  • Person.prototype = { ... } 是整体覆盖,会把原来的 constructor 一起覆盖掉,必须手动写 constructor: Person 指回构造函数。
  • 区分两个名字:prototype 是原型对象(构造函数身上的),__proto__ 是原型(实例对象身上的);__proto__ 非标准属性,现代浏览器显示为 [[Prototype]],且不要手动修改,影响性能。
  • 成员查找顺序:自身 → Person.prototype → Object.prototype → null,这就是原型链。

示例二:原型继承 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>原型继承</title>
</head>
<body>
  <script>
    // 1. 父级:抽取公共的属性和方法
    function Person() {
      this.eyes = 2
    }
    Person.prototype.eat = function () {
      console.log('我会吃饭')
    }

    // 2. 子级构造函数
    function Man() {}
    function Woman() {}

    // 3. 借助原型对象实现继承
    Man.prototype = new Person()
    Man.prototype.constructor = Man     // 手动指回构造函数

    Woman.prototype = new Person()
    Woman.prototype.constructor = Woman

    // 4. 子级原型上新增方法,不影响兄弟类
    Woman.prototype.baby = function () {
      console.log('我会生孩子')
    }

    const zs = new Man()
    const xl = new Woman()

    zs.eat()      // 继承自 Person.prototype
    xl.eat()      // 继承自 Person.prototype
    xl.baby()     // Woman 自己扩展的方法

    console.log(zs.baby)                       // undefined,Man 没有这个方法
    console.log(zs instanceof Man)             // true
    console.log(zs instanceof Person)          // true,构造函数原型在原型链上
    console.log(xl instanceof Man)             // false
  </script>
</body>
</html>

关键行与易错点

  • Man.prototype = new Person():继承的本质是把子级原型指向一个父级实例,这样子级实例沿 __proto__ 就能找到 Person.prototype 上的 eat。
  • 赋值后必须补一句 Man.prototype.constructor = Man,否则 constructor 会指向 Person。
  • 顺序不能乱:Woman.prototype.baby = ... 必须写在 Woman.prototype = new Person() 之后,否则会被覆盖掉。
  • Woman 上扩展的 baby 只挂在 Woman.prototype 上,Man 的实例访问不到,这是各自原型独立的体现。
  • instanceof 检测的是“构造函数的 prototype 是否在实例的原型链上”,所以 zs instanceof Person 也是 true。

示例三:reduce 计算薪资 + find / every / Array.from ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>数组常见方法</title>
</head>
<body>
  <div>1</div>
  <div>2</div>
  <div>3</div>

  <script>
    const employees = [
      { name: '张三', salary: 10000 },
      { name: '李四', salary: 15000 },
      { name: '王五', salary: 20000 }
    ]

    // 1. reduce 累加求薪资总和,初始值必须写 0
    const total = employees.reduce((prev, curr) => prev + curr.salary, 0)
    console.log(total)   // 45000

    // 2. find 找满足条件的第一个元素,findIndex 找它的索引
    const target = employees.find(item => item.name === '李四')
    const index = employees.findIndex(item => item.name === '李四')
    console.log(target, index)   // {name:'李四',salary:15000} 1

    // 3. every 要求全部满足,some 只要有一个满足
    const arr = [10, 20, 30]
    console.log(arr.every(item => item >= 10))   // true
    console.log(arr.some(item => item >= 30))    // true
    console.log(arr.some(item => item >= 40))    // false

    // 4. Array.from 把伪数组转成真数组
    const divs = document.querySelectorAll('div')   // 伪数组,没有 push
    const newArr = Array.from(divs)                 // 返回真数组,不影响原伪数组
    newArr.push('123')
    console.log(newArr)
    console.log(divs.length)   // 3,原来的伪数组没被改变
  </script>
</body>
</html>

关键行与易错点

  • reduce 回调里累加的是 curr.salary,不能直接写 prev + curr,否则就是对象相加得到字符串 "[object Object]..."。
  • 这里的初始值 0 不能省略:省略时第一次的 prev 是数组第一个对象,无法做数值累加。
  • 没有初始值时,reduce 的遍历次数比数组长度少一次(首元素直接当 prev);有初始值时,有几个元素就遍历几次,第一次的 prev 就是初始值。
  • 回调必须 return 上一次的结果,箭头函数一行时可省略 {} 和 return。
  • find 返回的是数组元素本身(对象),找不到返回 undefined;filter 返回的是数组,这是两者最容易混的点。findIndex 找不到返回 -1。
  • every 是“每个都满足才 true”,some 是“有一个满足就 true”,返回的都是布尔值。
  • Array.from 是挂在构造函数上的静态方法,写法是 Array.from(伪数组);它返回新数组,不修改原伪数组,转换后即可使用 push、every、find 等数组方法。

常见错误 ​

1. 方法写在构造函数体内,实例之间方法不共享

  • 现象:zs.sayHi === ls.sayHi 返回 false,实例越多内存占用越大。
  • 原因:this.sayHi = function(){} 在每次 new 时都重新创建一个函数对象,每个实例各持一份。
  • 修法:把方法挂到 Person.prototype 上,构造函数体内只保留实例独有的属性(name、age)。

2. 用对象字面量重写 prototype 后,constructor 丢失

  • 现象:Person.prototype.constructor === Person 变成 false,指向了 Object。
  • 原因:Person.prototype = { sing(){}, dance(){} } 是整体赋值,把原来带 constructor 的原型对象整个替换掉了。
  • 修法:在字面量里手动补 constructor: Person;或者逐个 Person.prototype.xxx = ... 赋值。

3. 原型方法写成箭头函数,this 拿不到实例

  • 现象:Person.prototype.sayHi = () => console.log(this.name) 输出 undefined 或指向 window。
  • 原因:箭头函数没有自己的 this,取的是定义时外层作用域的 this,不会随调用者(实例)改变。
  • 修法:原型方法一律用普通函数(或 ES6 方法简写 sayHi(){});箭头函数只用于不需要 this 的回调场景。

4. 原型继承先加方法、后赋值原型,方法被覆盖

  • 现象:Man.prototype.xxx 明明写了,实例却调不到,或 Man.prototype 里的内容全没了。
  • 原因:Man.prototype = new Person() 是整体替换,之前挂在 Man.prototype 上的成员全部被丢弃。
  • 修法:先完成 Man.prototype = new Person() 与 Man.prototype.constructor = Man,再往上追加子类特有方法。

5. reduce 处理对象数组时省略初始值,或累加错了字段

  • 现象:结果是 "[object Object][object Object]" 字符串拼接或 NaN。
  • 原因:没有初始值时,prev 取数组第一个元素(一个对象),prev + curr 变成对象运算;或写成 prev + curr 而不是 prev + curr.salary。
  • 修法:必须传第二个参数 0 作为初始值,累加项取 curr.salary 这类具体数值字段。

6. find / findIndex 的返回值判断写错

  • 现象:if (arr.findIndex(...)) 在找到下标 0 的元素时被判为不存在;或把 find 的返回值当数组去 .length。
  • 原因:findIndex 找不到返回 -1,找到可能是 0;find 找不到返回 undefined,找到返回的是元素本身而非数组。
  • 修法:findIndex 用 !== -1 判断;find 用 !== undefined 判断;需要多个结果时改用 filter。

7. Array.from 忘记接收返回值

  • 现象:Array.from(divs) 写完后 divs.push(...) 依然报 "not a function"。
  • 原因:Array.from 是静态方法,返回一个新数组,不修改原类数组对象。
  • 修法:const arr = Array.from(divs),后续操作 arr;原类数组保持不变是特性而非 bug。

练习 ​

练习 1(构造函数 + 原型) 写出 Person 构造函数(name、age),要求两个实例 zs、ls 调用 sayHi 输出 hi~,且满足 zs.sayHi === ls.sayHi。再用对象字面量方式重写 Person.prototype,同时提供 sing、dance 两个方法,保证 Person.prototype.constructor === Person 仍为 true。

练习 2(reduce 求薪资) 给定 [{name:'张三',salary:10000},{name:'李四',salary:15000},{name:'王五',salary:20000}],用 reduce 的箭头函数写法求当月总薪资。回答两个问题:① 第二个参数 0 能不能省?省略后会发生什么?② 如果把 prev + curr.salary 误写成 prev + curr,结果是什么?

练习 3(find / findIndex) 有一个商品对象数组,要求:① 找出第一个价格大于 100 的商品对象;② 找出该商品在数组中的下标;③ 写出一段代码,正确判断"价格大于 1000 的商品是否存在",并说明为什么不能直接写 if (arr.findIndex(fn))。

练习 4(原型继承) 定义父构造函数 Person(eyes = 2,原型上有 eat),子构造函数 Man、Woman,让二者继承 Person,再给 Woman 单独加 baby 方法。要求:zs instanceof Person 为 true,Man.prototype.constructor === Man,且 Woman 的 baby 不会出现在 Man 的实例上。写出完整代码并说明关键两步的顺序。

练习 5(every / some + Array.from) 用 document.querySelectorAll('div') 获取元素,先用 every 检测是否每个元素的 dataset.index 都存在,再用 some 检测是否存在某个特定 index。要求代码能正常调用 every/some,并说明为什么不能直接对 querySelectorAll 的返回值调用这两个方法。

原视频对照 ​

#视频时长
1数组reduce方法基本使用08:50
2数组reduce执行过程08:46
3reduce计算薪资小案例06:48
4数组find和findIndex方法12:09
5数组every和some方法05:56
6Array.from转换真数组05:34

小结 ​

  1. 面向对象在前端更多体现在"数据 + 行为"的组织上,构造函数负责封装实例独有的属性,原型负责承载共享的方法,二者分工不能混。
  2. prototype(原型对象)、__proto__(原型,即 [[Prototype]])、constructor(指回构造函数)三者是同一套机制的不同视角,任何一次整体替换 prototype 都要补回 constructor。
  3. 原型链决定了成员查找的路线:自身 → 原型对象 → Object 原型 → null;instanceof 判断的正是构造函数的 prototype 是否在这条链上。
  4. 原型继承的关键顺序是:先赋值 Child.prototype = new Parent(),紧接着修正 constructor,最后再追加子类独有方法。
  5. reduce 的两个参数决定了它的行为差异:有无初始值会影响遍历次数和第一次的 prev 来源;处理对象数组时初始值 0 和"取哪个字段累加"是两个必错点。
  6. find 返回元素、filter 返回数组、findIndex 返回下标(找不到为 -1)、every/some 返回布尔值,返回值类型不同决定了后续判断写法不能通用。
  7. Array.from 是挂在构造函数上的静态方法,返回新真数组且不改变原类数组;类数组(arguments、querySelectorAll、children)拿到后先转换再使用数组方法。
  8. 这些 API 不需要死记,遇到不熟悉的先查 MDN,确认三件事:作用、参数含义、返回值——这比背语法更可靠。