主题
Date与Array(Day10 2/2)
本页对应 Day10_JavaScript,共 7 段视频,约 3 小时 33 分。
学习目标
- 能区分 String 实例属性/方法与 String 构造函数属性/方法,并使用
charAt、indexOf、lastIndexOf、slice、substring、substr、split、toUpperCase、toLowerCase、charCodeAt、String.fromCharCode完成指定字符串处理。 - 能说明 Math 是内置对象,使用
Math.PI、abs、pow、sqrt、floor、ceil、round、max、min,并推导和编写取[0,n]、[m,n]随机整数的表达式,验证边界结果。 - 能实例化 Date 对象,使用无参、日期字符串、数字参数和时间戳形式,调用
getFullYear、getMonth、getDate、getDay、getHours、getMinutes、getSeconds、getMilliseconds、getTime及对应set/UTC方法,按指定格式输出日期时间并说明月份从 0 开始、时间戳为毫秒数的规则。 - 能区分数组修改器方法与访问器方法,使用
push、pop、unshift、shift、splice、sort、reverse修改原数组,使用concat、slice、join、indexOf、lastIndexOf返回结果且不修改原数组,并说明各方法返回值。 - 能使用
forEach、filter、map、every、some、reduce、reduceRight处理数组,编写筛选、映射、条件判断、累计求和等代码,并解释回调参数、返回值以及reduce省略初始值时的执行差异。 - 能依据引用类型相等规则解释
indexOf查找对象元素时返回-1的原因,并通过复用同一变量引用使查找成功,验证对象内容相同但引用不同不相等。
知识点
为什么单独学 Date
前面学 String、Math 时,我们已经反复区分“构造函数本身的方法”和“实例的方法”。Date 同样要这样看:Date 是构造函数,new Date() 得到的是日期时间对象,也就是 Date 的实例。它专门描述某个具体时刻。转写稿里口误说成“data”,这里统一纠正为 Date。
先想清楚:不传参数时,new Date() 包含的是“执行代码那一刻”的当前日期时间;传参数时,可以指定某个日期时间。调试工具里它可能显示成类似字符串的样子,但类型仍是对象。
js
const d1 = new Date();
console.log(d1); // 例如:2023-04-25T06:58:... GMT+0800易错点:不要因为控制台显示得像字符串,就以为 d1 是字符串;判断类型可用 typeof d1,结果是 "object"。
实例化 Date 的几种写法
为什么需要?实际业务经常要表示“某个指定时间”,不能永远用当前时间。怎么想?参数形式主要有:不传参、传日期时间字符串、传年/月/日/时/分/秒数字、传时间戳。怎么做?
js
const d2 = new Date("1995-12-17T10:06:45");
const d3 = new Date(2008, 9, 12, 10, 6, 45);
const d4 = new Date(1949, 10, 1, 10, 0, 12);
const d5 = new Date(1997, 7, 6); // 1997年8月6日 00:00:00易错点:用数字传月份时,月份从 0 开始,0 表示 1 月,10 表示 11 月。转写稿演示 new Date(1949, 10, 1, ...) 时,调试发现不是 10 月而是 11 月,原因就在这里。字符串形式里个位数通常要补零,如 09;数字形式不要补零,直接写 9。另外 new Date(1997) 只传一个数字时,不是“1997 年”,而是按时间戳解释。
时间戳与调试过程
为什么需要时间戳?因为年月日、时分秒之间进制不统一,直接做时间差很麻烦。转写稿里说得很清楚:先把时间换算成毫秒数,再计算。JS 时间戳指从 1970 年 1 月 1 日 0 时 0 分 0 秒(UTC)到目标时刻的毫秒数。
调试时写 new Date(36000000),结果仍在 1970 年;继续补零到更大的数,才逐渐到 1971 年、1981 年。这说明参数太小,连 1970 年都没过完。注意 JS 用的是毫秒,不是秒;其他语言常说“秒级时间戳”,JS 里要除以 1000 才是秒。
js
console.log(new Date(36000000)); // 仍是1970年附近
console.log(Date.now()); // 当前时间戳,毫秒易错点:不要把“时间戳”和“日期字符串”混用;只传一个数字参数时,Date 会把它当时间戳。
获取年月日时分秒
为什么需要?直接把 Date 对象给用户看,用户不一定懂。业务常要拼成“2023年4月25日”“15点22分”这种格式。怎么想?Date 实例提供一组 get... 方法,分别取年、月、日、星期、时、分、秒、毫秒。
js
const d = new Date();
console.log(d.getFullYear()); // 公元纪年,推荐
console.log(d.getMonth() + 1); // 月份0~11,要加1
console.log(d.getDate()); // 几号
console.log(d.getDay()); // 星期几,0表示周日
console.log(d.getHours());
console.log(d.getMinutes());
console.log(d.getSeconds());
console.log(d.getMilliseconds());
console.log(d.getTime()); // 时间戳易错点:getYear() 有 1900 偏移问题,转写稿演示得到 123 这类值,不推荐;用 getFullYear()。getMonth() 必须加 1。getDay() 是星期,不是几号;几号用 getDate()。时、分、秒、毫秒的单词是复数形式:Hours、Minutes、Seconds、Milliseconds。
UTC 时间与 set 修改
为什么需要 UTC?做外贸、跨时区展示时,要知道零时区时间。怎么想?在 get... 前加 UTC,如 getUTCFullYear()、getUTCHours()。东八区比零时区早 8 小时,所以转写稿里本地 15 点,UTC 是 7 点;分钟和秒通常同步。
js
console.log(d.getUTCFullYear());
console.log(d.getUTCHours());Date 还有 set... 方法,用来修改实例本身,例如 setFullYear(2000)、setUTCHours(12)、setTime(时间戳)。易错点:set 会改调用它的对象,属于“修改器”式操作;改本地时间或 UTC 时间会互相对应。setTime 参数仍是毫秒时间戳。
构造函数本身的方法
为什么需要?有时不想先创建实例,只想拿当前时间戳或指定日期的时间戳。怎么做?用静态方法 Date.now() 和 Date.UTC(...)。
js
console.log(Date.now()); // 当前时间戳
console.log(Date.UTC(2000, 9, 22, 10)); // 指定UTC日期的时间戳易错点:Date.now 没有参数;Date.UTC 的参数形式类似 new Date(年,月,日,时,分,秒),月份同样从 0 开始。还要区分 Date.UTC 和实例的 getTime():前者是构造函数本身的方法,后者是实例方法。
先分清:修改器方法与访问器方法
为什么需要:数组方法很多,死记容易混。先判断“方法执行后原数组变不变”,再记返回值,才能把一堆方法串起来。
怎么想:修改器方法会改调用它的数组本身;访问器方法不改原数组,只把计算结果返回。
怎么做:先看方法名和返回值,再用小例子验证原数组是否变化。
易错点:不要只看“有没有返回值”。修改器方法也可能有返回值,例如 push 返回新长度,pop 返回被删元素。
js
const arr = ['曹操', '刘备', '孙权'];
console.log(arr.push('吕布')); // 4,返回新长度
console.log(arr); // 原数组已变修改器方法:push、unshift、pop、shift、splice、sort、reverse
为什么需要:实际开发常要在数组头尾增删、替换元素、排序和翻转。
怎么想:push/unshift 是加,pop/shift 是删;splice 是“删几个、再加几个”;sort/reverse 会直接改原数组。
怎么做:记住返回值:push/unshift 返回新长度;pop/shift 返回被删除的元素;splice 返回被删除元素组成的新数组,哪怕没删也返回空数组;sort/reverse 返回改后的数组,同时原数组也变了。
易错点:splice(开始索引, 删除个数, 新元素...) 第二个参数是删除个数,不是结束索引;sort() 默认按字符串 Unicode 排序,数字排序要传比较函数。
js
const list = ['司马', '六胡', '东方'];
console.log(list.splice(1, 1, '刘', '牛')); // ['六胡']
console.log(list); // ['司马', '刘', '牛', '东方']访问器方法:concat、slice、join、indexOf、lastIndexOf
为什么需要:连接数组、截取片段、拼字符串、判断元素是否存在,都不应随便改原数组。
怎么想:这类方法“算完给结果”,原数组保持不动。concat 连接多个数组;slice 截取片段;join 合并成字符串;indexOf/lastIndexOf 找位置。
怎么做:slice(开始, 结束) 顾头不顾尾;join(分隔符) 不写默认逗号;indexOf 找不到返回 -1,所以常用 !== -1 判断包含。
易错点:indexOf 判断对象时比较的是引用,不是“长得一样”。两个对象字面量即使属性相同,也不是同一个对象。
js
const users = [{ name: '赵云' }];
console.log(users.indexOf({ name: '赵云' })); // -1
const zhao = { name: '赵云' };
console.log([zhao].indexOf(zhao)); // 0高阶方法:forEach、filter、map
为什么需要:遍历数组时,经常要“筛选一部分”或“改造每个元素”。
怎么想:forEach 只遍历,无返回值;filter 的回调返回 true 就保留该元素;map 把每次回调的返回值收集成新数组。
怎么做:三者回调都能拿到 item, index, array,常用前两个。
易错点:forEach 不返回新数组;filter 不写 return 相当于返回 undefined,结果为空数组;map 新数组长度与原数组一致,但元素由回调返回值决定。
js
const users = [
{ name: '曹操', age: 57 },
{ name: '刘备', age: 37 }
];
console.log(users.filter(u => u.age > 40)); // 曹操
console.log(users.map(u => u.name)); // ['曹操','刘备']判断型高阶方法:every、some
为什么需要:判断“是否全部满足”或“是否存在一个满足”。
怎么想:every 要求每个回调都返回真,才返回 true;some 只要有一个回调返回真,就返回 true。
怎么做:every 适合“所有用户都是成年人”;some 适合“是否有用户住在南京”。
易错点:两者都会短路。every 遇到第一个假就停;some 遇到第一个真就停。因此回调执行次数不一定是数组长度。
js
const users = [{ age: 20 }, { age: 17 }];
console.log(users.every(u => u.age >= 18)); // false
console.log(users.some(u => u.age >= 18)); // true累计型高阶方法:reduce、reduceRight
为什么需要:求和、累计计算、把数组归并成一个结果。
怎么想:reduce 的回调第一个参数 prev 是“上一次回调的返回值”,第二个是当前元素。最终 reduce 的返回值就是最后一次回调的返回值。reduceRight 只是从右往左遍历。
怎么做:写 reduce((prev, item) => prev + item.age, 0) 时,0 是初始值,回调从第一个元素开始。
易错点:不传初始值时,回调少执行一次,第一个元素直接作为第一次的 prev,从第二个元素开始遍历。不要混淆“回调参数”和“reduce 的第二个参数”。
js
const users = [{ age: 57 }, { age: 37 }, { age: 27 }];
const total = users.reduce((prev, item) => prev + item.age, 0);
console.log(total); // 121一、连字符转小驼峰
为什么需要:接口字段、CSS 类名常写成 get-element-by-id 这种连字符形式,而 JS 方法名习惯用小驼峰 getElementById,所以需要封装转换函数。
怎么想:先把字符串按 - 切开,得到若干单词;第一个单词保持小写,后面的单词把首字母变大写,再拼回去。转写稿里提到的“slit”应纠正为 split,它的作用正是把字符串分割成数组。
怎么做:
js
function toCamel(str) {
const parts = str.split('-');
let result = parts[0];
for (let i = 1; i < parts.length; i++) {
result += parts[i].charAt(0).toUpperCase() + parts[i].slice(1);
}
return result;
}
console.log(toCamel('get-element-by-id')); // getElementById
console.log(toCamel('set-name-by-class-name')); // setNameByClassName易错点:第一个单词不要首字母大写;split('-') 的参数必须是字符串 '-';charAt(0) 取首字母,slice(1) 取剩余部分,别把整个单词都大写。
二、翻转字符串
为什么需要:判断回文、处理某些展示顺序时,需要把字符串倒过来。字符串本身没有 reverse 方法,数组才有。
怎么想:转写稿里强调 reverse 是数组的修改器方法,会翻转数组并返回翻转后的数组。所以先把字符串拆成字符数组,翻转后再合并回字符串。转写稿里的“reward”应纠正为 reverse。
怎么做:
js
function reverseStr(str) {
return str.split('').reverse().join('');
}
console.log(reverseStr('hello')); // olleh易错点:split('') 用空字符串,才能按字符拆开;join('') 也要用空字符串,否则会变成 o,l,l,e,h。不要直接写 str.reverse(),字符串没有这个方法。
三、随机取数组元素
为什么需要:随机点名、抽奖、随机展示一条数据,都需要从数组里随机取一个元素。
怎么想:Math.random() 返回 [0, 1) 的小数,乘数组长度后得到 [0, length),再用 Math.floor 向下取整,就得到 0 到 length - 1 的随机整数,正好是合法索引。转写稿里的“flow”应纠正为 floor。
怎么做:
js
const names = ['刘姥姥', '牛姥姥', '司马姥姥', '欧阳姥姥', '独孤姥姥', '令狐姥姥', '东方姥姥'];
function randomItem(arr) {
const index = Math.floor(Math.random() * arr.length);
return arr[index];
}
console.log(randomItem(names));易错点:不能写成 Math.random() * arr.length - 1,也不能用 Math.ceil 或 Math.round 随便替代;边界必须是 0 到 length - 1,否则可能取到 undefined。
四、格式化当前日期时间
为什么需要:直接输出 new Date() 不利于用户阅读,业务里常需要 2021-03-04 19:02:56 这种固定格式。
怎么想:用 Date 实例方法分别取年、月、日、时、分、秒,再拼成字符串。注意 getMonth() 从 0 开始,要加 1;getDate() 才是几号,getDay() 是星期几。小于 10 的数字要补 0。
怎么做:
js
function pad(n) {
return n < 10 ? '0' + n : '' + n;
}
function formatNow() {
const d = new Date();
const y = d.getFullYear();
const m = pad(d.getMonth() + 1);
const day = pad(d.getDate());
const h = pad(d.getHours());
const min = pad(d.getMinutes());
const s = pad(d.getSeconds());
return y + '-' + m + '-' + day + ' ' + h + ':' + min + ':' + s;
}
console.log(formatNow());易错点:月份一定加 1;补零不能只补月,日、时、分、秒都要补;不要用 getDay() 代替 getDate()。转写稿里的“Lice”应纠正为 length,“叉艾”应纠正为 charAt,这些拼写错误在作业里要避免。
先分清“实例方法”和“构造函数方法”
为什么需要:String、Math、Date、数组都有大量方法,先分清“谁的方法”,后面才不会记混。怎么想:看调用形式。msg.length 是字符串实例属性;String.fromCharCode() 才是构造函数本身的方法。怎么做:实例方法通过具体字符串调用,构造函数方法通过 String.xxx 调用。易错点:length 是实例属性,不是构造函数属性;String.prototype 上的方法给实例用;String.fromCharCode 的 from 不是表单 form。
js
const msg = 'hello,高考乐';
console.log(msg.length); // 实例属性
console.log(String.fromCharCode(98)); // 'b'String 常用实例方法
为什么需要:字符串要查位置、截取、分割、转大小写、取编码。怎么想:按返回值分类——返回字符、返回位置、返回新字符串、返回数组。怎么做:charAt 按索引取字符;indexOf/lastIndexOf 按内容找位置;slice/substring/substr 截取;split 分割;toUpperCase/toLowerCase 转大小写;charCodeAt 取 Unicode 编码。易错点:索引从 0 开始;indexOf 找不到返回 -1,所以可判断是否包含;slice/substring 顾头不顾尾;substr 第二参数是截取长度;split('') 会把每个字符拆成数组元素;中文逗号和英文逗号不同。
js
const msg = 'hello,高考乐';
console.log(msg.charAt(2)); // 'l'
console.log(msg.indexOf('高')); // 6
console.log(msg.slice(4, 8)); // 截取到索引8之前
console.log(msg.split(',')); // ['hello', '高考乐']
console.log(msg.charCodeAt(4)); // 'o' 的编码Math 对象与随机整数
为什么需要:做数学计算和随机数。怎么想:Math 是系统定义好的对象,不是构造函数,直接 Math.xxx 用。怎么做:Math.PI、abs、pow、sqrt、floor、ceil、round、max、min、random。随机 0 到 N:Math.floor(Math.random() * (N + 1));随机 M 到 N:Math.floor(Math.random() * (N - M + 1)) + M。易错点:random() 取 [0,1),取不到 1;floor 向下取整,ceil 向上取整,round 四舍五入;max/min 参数是多个数,不是数组。
js
Math.floor(Math.random() * 10); // 0~9
Math.floor(Math.random() * (15 - 8 + 1)) + 8; // 8~15Date 日期时间
为什么需要:展示时间、做时间计算。怎么想:Date 是构造函数,实例包含具体日期时间;月份从 0 开始。怎么做:new Date() 取当前时间;new Date(年,月,日,时,分,秒) 指定时间;用 getFullYear/getMonth/getDate/getDay/getHours/getMinutes/getSeconds/getMilliseconds/getTime 获取;Date.now() 取当前时间戳,Date.UTC() 取指定日期时间戳;setXxx 修改原对象。易错点:getMonth() 要 +1;getDay() 是星期几,0 为周日;getYear() 从 1900 算,别用;时间戳是毫秒;UTC 是零时区;set 会改原对象。
js
const d = new Date();
console.log(d.getFullYear(), d.getMonth() + 1, d.getDate());
console.log(d.getDay());
console.log(Date.now());数组修改器方法
为什么需要:原地增删改数组。怎么想:执行后调用该方法的数组本身会被修改。怎么做:push/pop/unshift/shift/splice/sort/reverse。易错点:push/unshift 返回新长度;pop/shift 返回被删元素;splice 返回被删元素组成的数组;sort 默认按字符串 Unicode 排序。
js
const arr = ['曹操', '刘备'];
arr.push('孙权'); // 返回长度
arr.pop(); // 返回被删元素
arr.splice(1, 1, '赵云'); // 返回被删元素数组数组访问器方法
为什么需要:不改原数组,得到新结果。怎么想:看返回值——新数组、字符串或位置。怎么做:concat 连接数组;slice 截取;join 合并字符串;indexOf/lastIndexOf 找位置。易错点:concat 可连接多个数组;slice 顾头不顾尾;join 默认逗号;找不到返回 -1;对象比较看引用,长得一样也不相等。
js
const a = [1, 2];
const b = [3];
console.log(a.concat(b)); // [1,2,3]
console.log(['a', 'b'].join('-')); // 'a-b'数组高阶方法
为什么需要:遍历、筛选、映射、判断、累计。怎么想:参数是回调函数。forEach 只遍历无返回值;filter 保留回调返回 true 的元素;map 用回调返回值组成新数组;every 全真才真;some 一真即真;reduce 做累计,返回值是最后一次回调的返回值;reduceRight 从右往左。易错点:map 不写 return 会得到 undefined 数组;every/some 会短路;reduce 不写初始值时从第二项开始,prev 是第一项。
js
const users = [
{ name: '曹操', age: 57 },
{ name: '刘备', age: 37 }
];
console.log(users.filter(u => u.age > 40));
console.log(users.map(u => u.name));
console.log(users.every(u => u.age >= 18));
console.log(users.some(u => u.name === '刘备'));
console.log(users.reduce((prev, u) => prev + u.age, 0));先看整体:原型解决什么问题
为什么需要:构造函数可以批量创建对象,但如果每个对象都各自保存一份方法,会重复占内存,也不利于统一修改。原型就是用来存放“这类对象共享的属性和方法”的地方。
怎么想:先分清三件事:对象是实例,构造函数负责描述和创建实例,原型是实例共享属性的来源。转写稿里反复让同学判断 String.prototype 是实例的还是构造函数本身的,并纠正“Lice”应为 length,这个判断习惯同样适用于原型:看到 .prototype,先问“它服务谁”。
怎么做:先记住两条获取原型的入口,再看三者关系。
易错点:不要把“构造函数的属性”和“构造函数原型上的属性”混为一谈;实例能用的,不一定在实例自己身上。
本节笔记核心结构如下,后面逐块展开:
js
1. 原型
1.1 获取原型
对象.__proto__
对象的构造函数.prototype
1.2 原型、对象、构造函数
① 对象和构造函数
构造函数是对象的描述、对象是构造函数的实例
一个构造函数对应无数个对象,一个对象只有一个构造函数
② 对象和原型
每个对象都有原型,对象可以使用原型的属性
一个对象只有一个原型,一个原型可以给多个对象当原型
② 对象的原型和对象的构造函数
通过构造函数的 prototype 属性可以获取到对象的原型
对象的构造函数相同,原型也相同
1.3 自定义构造函数
将方法添加到对象的原型上
1.4 对象.hasOwnProperty()
1.5 Object.create()
2. 原型链
2.1 原型链描述属性查找过程
2.2 原型链
顶层原型Object.prototype, 它没有原型,绝大部分对象的原型链顶端都是Object.prototype
Function 是所有函数的构造函数, 自己的构造函数也是 Function
2.3 特殊
Object 的原型 Function.prototype, Function.prototype 的构造函数是 Object
Function.prototype === Function.__proto__
var arr = [];
Function.prototype 不在 arr 的原型链上
Function.prototype 在 Array 的原型链上获取原型:两条入口要分清
为什么需要:只有先拿到原型,才能验证“实例共享的方法到底放在哪里”。
怎么想:实例可以直接用 对象.__proto__ 看自己的原型;也可以从构造函数入口看:构造函数.prototype。两者指向同一类对象的原型。
怎么做:
js
function Person(name) {
this.name = name;
}
const p = new Person('张三');
console.log(p.__proto__ === Person.prototype); // true易错点:__proto__ 是对象身上的入口,prototype 是构造函数身上的入口。不要写成 p.prototype 去取实例原型,实例本身没有“prototype”这个共享入口。
对象、构造函数、原型三者的关系
为什么需要:原型链的很多混淆,来自把“一个对象”和“一类对象”混在一起。
怎么想:构造函数是描述,实例是按描述创建出来的对象。一个构造函数可以创建无数实例;一个实例只有一个构造函数来源。一个实例只有一个原型,但一个原型可以给多个实例共享。
怎么做:用同一构造函数创建两个对象,验证它们原型相同:
js
const p1 = new Person('张三');
const p2 = new Person('李四');
console.log(p1.__proto__ === p2.__proto__); // true
console.log(p1.__proto__ === Person.prototype); // true易错点:p1 和 p2 是两个独立对象,但共享同一个原型。不要因为“原型相同”就认为它们是同一个对象。
自定义构造函数:方法放到原型上
为什么需要:如果方法写在构造函数内部,每个实例都会有一份自己的函数,浪费内存。
怎么想:实例独有的数据放实例上,实例共享的方法放原型上。
怎么做:
js
function Person(name) {
this.name = name;
}
Person.prototype.say = function () {
console.log(this.name);
};
const p = new Person('张三');
p.say(); // 张三易错点:this.name 是实例属性;say 是原型方法。调用 p.say() 时,this 仍指向调用它的实例 p。
hasOwnProperty 与 Object.create
为什么需要:属性可能来自实例自身,也可能来自原型。需要判断“是不是自己的”。
怎么想:hasOwnProperty 只检查对象自身,不查原型链;Object.create(obj) 可以指定新对象的原型。
怎么做:
js
function Person(name) {
this.name = name;
}
Person.prototype.say = function () {};
const p = new Person('张三');
console.log(p.hasOwnProperty('name')); // true
console.log(p.hasOwnProperty('say')); // false
const obj = Object.create(p);
console.log(obj.__proto__ === p); // true易错点:hasOwnProperty 对原型上的 say 返回 false,这不是“没有 say”,而是“say 不在自身”。
原型链:属性查找过程
为什么需要:访问一个属性时,JS 不只查对象自己,还会沿着原型往上找。
怎么想:先在自身找,找不到去原型找,再找不到去原型的原型找,直到 Object.prototype;还没有就返回 undefined。
怎么做:把查找过程画成链:实例 → 构造函数.prototype → Object.prototype。
易错点:顶层原型是 Object.prototype,它没有原型;绝大部分对象的原型链顶端都是它。不要以为所有对象都直接挂在 Object.prototype 上。
特殊关系:Object、Function 与 Array
为什么需要:学到 Function、Object、Array 时,原型关系会出现“看起来绕”的情况。
怎么想:Function 是所有函数的构造函数,自己的构造函数也是 Function;Object 的原型是 Function.prototype,Function.prototype 的构造函数是 Object;并且 Function.prototype === Function.__proto__。
怎么做:用代码验证特殊关系和数组链:
js
console.log(Function.prototype === Function.__proto__); // true
var arr = [];
// Function.prototype 不在 arr 的原型链上
// Function.prototype 在 Array 的原型链上易错点:不要把“函数能调用”与“数组实例链”混在一起。数组实例的原型链经过 Array.prototype,再往上到 Object.prototype;Function.prototype 不属于普通数组实例的直接原型链。
调试与易错点清单
为什么需要:原型题最容易“背结论”,一换对象就乱。
怎么想:每次只问三个问题:它是实例还是构造函数?属性在自身还是在原型?当前查找走到哪一层?
怎么做:用 === 验证原型关系,用 hasOwnProperty 区分自身属性,用 Object.create 手动指定原型做小实验。
易错点:把 prototype 和 __proto__ 混用;把“原型相同”当成“对象相同”;把 Function.prototype 误认为在 arr 的原型链上。按笔记里的特殊关系逐条验证,比死记更稳。
先分清:原始值不是对象,对象不是“值本身”
为什么需要:后面写赋值、比较、修改时,很多“看起来一样却不等”“改了一个另一个也变”的问题,根源都在这里。课堂里反复强调“实例”和“构造函数”的区别,也是在训练同一个判断习惯:先看它到底是什么类型。
怎么想:原始类型(string、number、boolean、null、undefined、symbol、bigint)叫值类型;对象类型(object、array、function 等)叫引用类型。值类型关注“值”,引用类型关注“地址”。
怎么做:先判断数据是不是对象。是对象,就不要把它当成一坨普通值来想,而要想到“变量里存的是地址”。
易错点:不要因为对象打印出来像字符串,就把它当字符串;也不要把“变量”当成数据本身。变量只是容器,容器里装的可能是值,也可能是地址。
内存存储方式:栈里放值,堆里放对象
为什么需要:不理解存储位置,就很难理解后面的赋值和判等。笔记里写得很清楚:
1. 内存存储方式
值类型: 存储在栈结构
引用类型: 存储在堆结构,栈结构中存储的是地址怎么想:值类型的数据直接把值放在栈里;引用类型真正的内容放在堆里,栈里只保存一个指向堆的地址。可以想成:值类型是“杯子里直接装饮料”,引用类型是“杯子里放一张写着仓库门牌号的纸条”。
怎么做:看到对象、数组,先意识到“变量里是地址”;看到数字、字符串,先意识到“变量里就是值”。
易错点:不要以为栈里存了对象的全部内容。栈里存的是地址,真正属性在堆里。
赋值方式:值传递 vs 引用传递
为什么需要:赋值是最常见的操作,但值类型和引用类型赋值后的表现不同。
怎么想:值类型赋值,是把值复制一份;引用类型赋值,是把地址复制一份。笔记写:
2. 赋值方式(传值方式)
值类型: 值传递
引用类型: 引用传递(地址传递)怎么做:值类型赋值后,两个变量各自独立;引用类型赋值后,两个变量可能指向同一个对象。若通过一个变量修改对象属性,另一个变量再看,会看到变化,因为地址相同。
易错点:把引用类型变量重新赋值为一个新对象,不等于修改了原对象;它只是让这个变量改指向新地址。另一个仍指向旧地址的变量不受影响。
可变与不可变:整体不能改一部分,集合可以改属性
为什么需要:这解释了为什么字符串“改不了”,而对象“能改”。
怎么想:笔记说:
3. 可变和不可变
值类型: 也叫不可变类型,值类型都是整体,不能修改其中的一部分。
引用类型: 也叫可变类型,引用类型是集合,可以修改其中的某个属性,修改完之后还是原来的对象怎么做:字符串方法通常返回新字符串,原字符串不变;对象可以 obj.name = ... 修改属性,修改后仍是原对象。课堂里讲数组“修改器方法”时也体现这一点:push、pop、splice 等会改变调用它的数组本身;访问器方法则把结果作为返回值给出,不修改原数组。
易错点:值类型“不能改一部分”,不是变量不能重新赋值。变量可以换一个新值,但原来的值没有被切开修改。引用类型改属性后还是原对象,不是自动变成新对象。
判等方式:值相同 vs 地址相同
为什么需要:比较是最容易出错的地方。笔记写:
4. 判等方式
值类型: 只要值是相同的,两个数据就相等
引用类型: 只有地址相同,两个数据才相等。怎么想:值类型看“内容”;引用类型看“是不是同一个”。课堂调试中,用数组 indexOf 查对象:参数写了一个字面量对象,即使它和数组里某个对象“长得一样”,结果仍是 -1;反复解释:每实例化一次,就在内存中创建独立空间,长得再像也不是同一个。后来把同一个对象先存到变量里,再用变量去查,才查到,因为变量里保存的是同一个地址。
怎么做:比较对象前先问“是不是同一个引用”。需要按内容比较时,不能直接靠 === 判断两个对象“长得一样”,要自己比较关键属性,或使用同一引用。
易错点:{} === {} 为假;两个属性完全相同的对象也不相等。只有指向同一地址才相等。不要把“值相等”的规则套到对象上。
课堂画面






代码示例
示例一:数组实例的修改器方法与访问器方法
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>数组实例的修改器方法与访问器方法</title>
</head>
<body>
<script>
// 准备数据:7 个姥姥
let nameList = ['刘姥姥', '牛姥姥', '司马姥姥', '欧阳姥姥', '独孤姥姥', '令狐姥姥', '东方姥姥'];
console.log('原始数组:', nameList); // 7 个元素
// ===== 修改器方法:执行后调用它的数组本身会被修改 =====
console.log('push 返回值:', nameList.push('爱新觉罗姥姥')); // 8 —— 返回添加后的长度
console.log('unshift 返回值:', nameList.unshift('纳兰姥姥')); // 9 —— 返回添加后的长度
console.log('pop 返回值:', nameList.pop()); // 爱新觉罗姥姥 —— 返回被删除的元素
console.log('shift 返回值:', nameList.shift()); // 纳兰姥姥 —— 返回被删除的元素
console.log('增删之后:', nameList);
// splice(开始索引, 删除个数, 要添加的元素...)
// 删掉索引 2 的元素,同时插入两个新元素;返回由被删除元素组成的数组
console.log('splice 返回值:', nameList.splice(2, 1, '刘姥姥', '牛姥姥'));
console.log('splice 之后:', nameList);
console.log('sort 返回值:', nameList.sort()); // 按 Unicode 编码排序,返回排序后的数组
console.log('reverse 返回值:', nameList.reverse()); // 翻转数组,返回翻转后的数组
// ===== 访问器方法:不修改原数组,结果通过返回值给出 =====
const nums = [10, 20, 30];
const joined = nameList.concat(nums, ['A', 'B']);
console.log('concat 结果:', joined); // 多个数组连接成一个新数组
console.log('nameList 没变:', nameList); // 原数组不受影响
console.log('slice 结果:', nameList.slice(1, 3)); // 顾头不顾尾,取索引 1、2
console.log('join 结果:', nameList.join('-')); // 用分隔符把元素合并成一个字符串
console.log('indexOf:', nameList.indexOf('欧阳姥姥')); // 第一次出现的索引,不存在返回 -1
console.log('lastIndexOf:', nameList.lastIndexOf('欧姥姥')); // -1 表示不存在
</script>
</body>
</html>要点解释与易错点:
- 修改器与访问器的区别:
push、pop、unshift、shift、splice、sort、reverse执行后原数组本身被修改;concat、slice、join、indexOf、lastIndexOf不修改原数组,结果只在返回值里。 - 修改器也有返回值:
push、unshift返回新长度,pop、shift返回被删除的那个元素,splice永远返回数组(不删除就是空数组),sort和reverse返回的就是数组本身。 slice顾头不顾尾:slice(1, 3)取索引 1 和 2,不包含索引 3;只写一个参数则截取到最后。indexOf用-1判断存在性:索引0表示第一个元素,只有-1才表示不存在。sort默认按 Unicode 编码排序,中文排序结果不符合直觉,不要指望它按拼音排序。
示例二:数组的高阶函数(filter / map / every / some / reduce)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>数组高阶函数</title>
</head>
<body>
<script>
const users = [
{ name: '曹操', age: 57, address: '南京' },
{ name: '刘备', age: 37, address: '上海' },
{ name: '孙权', age: 27, address: '南京' },
{ name: '吕布', age: 47, address: '北京' },
{ name: '赵云', age: 67, address: '上海' },
{ name: '诸葛亮', age: 47, address: '成都' }
];
// filter:回调返回 true 的元素,组成新数组
const adults = users.filter(function (item, index) {
return item.age >= 40;
});
console.log('filter 结果:', adults);
// map:回调的返回值,组成与原数组等长的新数组
const names = users.map(function (item) {
return item.name;
});
console.log('map 取名字:', names);
// map 改造元素:结构不变,只把 age 减 10
const younger = users.map(function (item) {
return { name: item.name, age: item.age - 10, address: item.address };
});
console.log('map 减 10 岁:', younger);
// every:每个回调都返回 true 才为 true,遇到 false 立即停止
console.log('every 都成年:', users.every(function (item) {
return item.age >= 18;
}));
// some:只要有一个回调返回 true 就为 true,遇到 true 立即停止
console.log('some 有人住南京:', users.some(function (item) {
return item.address === '南京';
}));
// reduce:累计运算,第二个参数是初始值
const total = users.reduce(function (prev, item, index, arr) {
return prev + item.age;
}, 0);
console.log('年龄和:', total);
// reduce 不写初始值:少执行一次,第一个元素直接当 prev
const nums = [12, 34, 5, 67, 89];
console.log('数组和:', nums.reduce(function (prev, item) {
return prev + item;
}));
</script>
</body>
</html>要点解释与易错点:
filter是“过滤”:回调返回true的元素进入新数组,返回false的被丢弃;回调不写return相当于返回undefined,结果是空数组。map是“映射”:新数组长度一定与原数组一致,元素由回调的返回值决定;回调不写return就会得到一串undefined。every/some有短路特性:every遇到第一个false就停止并返回false;some遇到第一个true就停止并返回true,所以回调的执行次数不一定是数组长度。reduce的prev是上一次回调的返回值:第一次执行时prev取第二个参数(初始值);不写初始值时,回调少执行一次,第一个元素直接当prev,从第二个元素开始遍历。reduce的返回值是最后一次回调的返回值,理解这一点才能用好累计运算。
示例三:Math 随机数 + Date 日期格式化(作业 2、3、4)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>随机数与日期时间</title>
</head>
<body>
<script>
// ===== 作业 2:翻转字符串,用上数组的 reverse 方法 =====
function reverseStr(str) {
return str.split('').reverse().join('');
}
console.log(reverseStr('hello')); // olleh
// ===== 作业 3:参数是数组,返回随机取出来的元素 =====
const names = ['刘姥姥', '牛姥姥', '司马姥姥', '欧阳姥姥', '独孤姥姥', '令狐姥姥', '东方姥姥'];
function getRandomElement(arr) {
// Math.random() 取 [0,1) 的小数,乘长度再向下取整,得到 0 ~ length-1 的随机整数
const index = Math.floor(Math.random() * arr.length);
return arr[index];
}
console.log('随机元素:', getRandomElement(names));
// 扩展:取 m ~ n 之间的随机整数
function getRandomInt(m, n) {
return Math.floor(Math.random() * (n - m + 1)) + m;
}
console.log('8~15 的随机整数:', getRandomInt(8, 15));
// ===== 作业 4:输出当前日期时间 2021-03-04 19:02:56(字符串拼接) =====
function pad(num) {
return num < 10 ? '0' + num : '' + num;
}
function formatDate() {
const d = new Date();
const y = d.getFullYear(); // 年
const m = d.getMonth() + 1; // 月:从 0 开始,必须加 1
const day = d.getDate(); // 几号:是 getDate 不是 getDay
const h = d.getHours(); // 时
const mi = d.getMinutes(); // 分
const s = d.getSeconds(); // 秒
return y + '-' + pad(m) + '-' + pad(day) + ' '
+ pad(h) + ':' + pad(mi) + ':' + pad(s);
}
console.log('当前时间:', formatDate());
// 时间戳相关
console.log('当前时间戳(毫秒):', Date.now());
console.log('时间戳 0 对应:', new Date(0).getFullYear()); // 1970
</script>
</body>
</html>要点解释与易错点:
Math.random()范围是[0, 1),取不到 1;所以Math.random() * arr.length的结果永远小于长度,Math.floor之后最大是length - 1,不会越界。- 取
m ~ n的随机整数:先取0 ~ (n-m)再加m,即Math.floor(Math.random() * (n - m + 1)) + m,括号里必须+1,否则取不到n。 getMonth()从 0 开始(0 是 1 月,11 是 12 月),展示给用户前一定要+1;其余年、日、时、分、秒都是从正常数值开始。getDay()返回的是星期几,getDate()才是几号,这两个方法名最容易混。- 补零要用字符串拼接,
num < 10时在前面拼'0',否则19:2:56这种格式就不对。 split('')把字符串拆成字符数组,再reverse()翻转,最后join('')拼回字符串——这正是split与join互为逆操作。
常见错误
现象:把
length、slice等当成String构造函数方法调用。→ 原因:未区分实例/原型成员与构造函数静态成员。→ 修法:先看调用形式,str.xxx查实例/原型,String.xxx才查构造函数;用String.prototype验证归属。现象:
substr(4, 8)以为截取到索引 8。→ 原因:混淆slice/substring的结束索引与substr的长度参数。→ 修法:优先统一用slice(start, end);必须用长度时先换算,并写边界用例验证。现象:用
if (str.indexOf(key))判断包含。→ 原因:忽略返回-1与0的布尔语义。→ 修法:显式写str.indexOf(key) !== -1。现象:对象数组用
indexOf({...})查找失败。→ 原因:引用类型按地址比较,字面量对象每次都是新地址。→ 修法:保存同一引用再查,或用find/findIndex按业务属性判断。现象:以为
forEach能返回过滤或映射后的新数组。→ 原因:混淆forEach与filter/map的返回值契约。→ 修法:只做遍历用forEach;要新数组用filter/map;要布尔判断用every/some;要累计用reduce。现象:
new Date(2023, 3, 25)得到 4 月却以为 3 月。→ 原因:Date 月份索引从 0 开始。→ 修法:构造和读取时统一按monthIndex处理,展示时再+1。现象:
Math.floor(Math.random() * n)想取1..n却总取不到n。→ 原因:未按起点做偏移。→ 修法:先取0..n-1,再整体加起点;空数组、边界值必须测试。
练习
封装
toCamel(str):把'get-element-by-id'转为'getElementById',把'set-name-by-class-name'转为'setNameByClassName'。封装
reverseString(str),要求使用数组reverse实现翻转;再封装randomPick(arr),随机返回数组元素,空数组返回undefined。给定
users数组,分别用filter/map/every/some/reduce完成:筛选成年用户、提取姓名、判断是否全成年、判断是否有南京用户、求年龄和;并说明这些方法是否修改原数组。封装
formatDate(date),输出YYYY-MM-DD HH:mm:ss,个位数补零;同时输出该日期的时间戳(毫秒)。判断并解释结果:
var a={x:1}; var arr=[a,{x:1}]; arr.indexOf({x:1}); arr.indexOf(a);再给出按属性查找对象的替代写法。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 内置构造函数-String | 33:18 |
| 2 | 内置对象-Math | 20:30 |
| 3 | 取随机数 | 20:14 |
| 4 | 内置构造函数-Date | 49:55 |
| 5 | 数组实例的修改器方法 | 12:28 |
| 6 | 数组实例的访问器方法 | 14:38 |
| 7 | 数组实例的访问器方法02 | 1:01:58 |
小结
- 方法归属先分清:实例/原型方法用实例调用,静态方法用构造函数调用。
- 字符串截取优先
slice,参数语义必须用测试用例锁定。 - 查找位置类 API 中
-1是“不存在”的唯一可靠信号。 - 数组方法先分修改器/访问器;数据处理优先选择无副作用的访问器方法。
- 回调方法按返回值契约选择:
forEach遍历、filter筛选、map改造/提取、every/some判断、reduce累计。 - Date 的月份索引、UTC/时区、毫秒时间戳是高频坑点,展示前统一格式化。
- 随机整数先定区间再套偏移,空数组和边界值必须防御。
- 引用类型比较看地址,对象数组查找优先按业务字段。