主题
JavaScript 课堂案例(Day8 1/4)
本页对应 Day08_JavaScript,共 6 段视频,约 40 分钟。
学习目标
- 能够使用
sort配合回调函数对对象数组按指定属性进行升序或降序排序,并说明回调函数参数与返回值对排序结果的影响。 - 能够区分 Object 对象的属性名与属性值,使用点语法和中括号语法完成属性读写,并判断何时必须使用中括号语法。
- 能够使用
for...in遍历 Object 对象的属性,正确获取属性名并通过obj[attr]读取对应属性值,避免误用obj.attr。 - 能够使用
delete运算符删除 Object 对象中的指定属性,并通过输出验证删除结果。 - 能够使用
in运算符判断指定属性名是否存在于 Object 对象中,并根据返回的布尔值进行条件判断。 - 能够识别属性名在
in表达式和中括号语法中作为变量与作为字符串字面量的区别,解释不加引号时产生变量查找或报错的原因。
知识点
函数回顾:调用、返回值、参数与作用域
为什么需要:后面所有案例都建立在函数上。函数把有特定功能的代码块封装起来,起名后复用;同时要分清“函数名是变量、函数是数据类型”。
怎么想:看到函数名后加小括号才是调用;匿名函数直接量后加小括号也能调用。调用表达式的值就是返回值;没有写 return 或只写 return,值为 undefined。形参是函数内部变量,实参按顺序赋值;实参多了白给,少了对应形参为 undefined。
怎么做:带默认值的形参放后面;需要接收全部实参时用 arguments;需要避免全局污染时用立即调用函数:
js
(function () {
// 模块内部代码
})();易错点:作用域只与声明语句所在位置有关,与调用位置无关;函数内部使用了某个变量,即使声明写在下面,也不会再往全局找。提升时 var 只提前创建不赋值,function 声明会连函数值一起提升。
使用 sort 对对象数组排序
为什么需要:后端返回的数据常是“元素为对象的数组”,例如用户信息含姓名、年龄、地址。直接 sort() 会按字符串规则排,不能按年龄排。
怎么想:对象之间不能直接比大小,要比对象属性,如 age。排序要写回调函数,让 sort 内部反复调用它决定顺序。
怎么做:课堂写法是返回 next.age - prev.age 得到升序:
js
users.sort(function (prev, next) {
return next.age - prev.age;
});
console.log(users);调试时先输出 prev、next,确认拿到的是对象,再输出 prev.age、next.age,最后写返回值。
易错点:不写回调函数会按字符串排;回调里不能拿两个对象直接相减;要明确升序还是降序,返回正负数会影响交换位置。
Object 对象:概念、创建与读写
为什么需要:数组是有序集合,用索引取值;对象是无序集合,用属性名描述信息,更适合表达“一个用户”。
怎么想:对象由属性组成,属性名通常是字符串,属性值是任意类型;属性值是函数时,该属性叫方法。创建首推对象直接量 {};属性名符合标识符规范时可省略引号。
怎么做:读写用点语法或中括号语法:
js
obj.userName;
obj['userName'];给不存在的属性赋值就是添加属性;读取不存在的属性得到 undefined。
易错点:点语法只适合符合标识符规范的属性名;中括号里写属性名要加引号,否则会被当成变量。需要用变量表示属性名时,必须用中括号且不加引号:obj[attr]。
遍历 Object 对象的属性
为什么需要:对象没有 length,也不能像数组那样用索引循环。
怎么想:for...in 专门用于遍历对象属性,对象有几个属性就循环几次。
怎么做:for...in 每次拿到的是属性名,不是属性值;值要用 obj[属性名] 取:
js
for (var attr in obj) {
console.log(attr); // 属性名
console.log(obj[attr]); // 属性值
}调试时若写成 obj.attr,会去找名为 attr 的属性,而不是变量 attr 的值,所以拿不到。
易错点:attr 是变量;点语法后跟变量名会被当成属性名。必须用 obj[attr],中括号内不加引号。
删除对象属性:delete
为什么需要:数组可按索引删元素,对象无序,只能按属性名删。
怎么想:delete 是一元运算符,有副作用,会真正改变对象结构。
怎么做:
js
delete user.userName;
delete user['home-address'];删除后再输出对象,属性数量会减少。
易错点:属性名不符合标识符规范时不能用点语法,如 home-address 要用中括号;对象没有数组那种“前、中、后”的删除方式。
判断属性是否存在:in
为什么需要:读取不存在属性得到 undefined,但属性值本身也可能为 undefined,所以需要专门判断属性名是否存在。
怎么想:属性名 in 对象 组成表达式,结果是布尔值:存在为 true,不存在为 false。
怎么做:
js
'userName' in user; // true
'drink' in user; // false易错点:属性名要加引号。写成 userName in user 时,userName 会被当成变量;若未声明就报错。调试时先判断“它是字符串还是变量”,就能避免这个坑。
函数组成、调用与返回值
为什么需要:函数是把“具有特定功能的代码块”封装起来,起个名字,下次用名字调用。转写稿强调:函数名本质上是变量,函数在 JS 中是一种数据类型。
怎么想:先分清组成:函数名、函数体、参数、返回值。调用不是“看见函数名”就执行,而是函数名后加小括号;匿名函数直接量后加小括号也会形成调用表达式。调用表达式的值就是返回值。
怎么做:
js
function sum(a, b) {
return a + b;
}
var r = sum(1, 2);
console.log(r); // 3return 负责设置返回值;没有写 return,或只写 return;,函数调用表达式会自动得到 undefined。return 还会立即结束函数,后面的代码不再执行。
易错点:把“函数名”当成调用;把“函数体执行”和“调用表达式取值”混在一起;忘记 return 会得到 undefined,不是报错。
函数参数:形参、实参、数量、默认值、arguments
为什么需要:函数要处理不同数据,就要通过参数把外部数据带进函数体。
怎么想:形参是创建函数时写的变量,实参是调用时真正传入的值。数量对不上 JS 不报错:实参多了“白给”,实参少了对应形参自动得到 undefined。默认值要放在参数列表后边,因为实参按顺序赋值,放前面意义不大。形参本身也是局部变量,只能在函数内用。arguments 是系统自动创建的类数组,只能在函数内用,能得到调用时传入的所有实参。
怎么做:
js
function demo(a, b = 2) {
console.log(a, b, arguments);
}
demo(1); // 1 2 [1]易错点:以为实参少了会报错;把默认值参数随便放前面;把 arguments 当真正数组;在函数外使用 arguments 或形参。
作用域与作用域链
为什么需要:变量不是哪里都能用,必须知道它的有效范围,否则会误读全局变量。
怎么想:函数内创建的变量是局部变量,函数外创建的是全局变量。函数嵌套声明会形成作用域链。使用某个变量时,先从本层找,找不到再找上层,一直到全局,都找不到才报错。也可以从创建变量角度看:本作用域和下级能用,上级用不了。作用域只与变量/函数声明语句所在位置有关,与调用位置无关。
怎么做:课堂用类似下面的代码验证“本作用域已有声明,就不会再往上找”:
js
var x = 10;
function f() {
console.log(x); // undefined
var x = 20;
}
f();因为函数内下面有 var x,提升后本作用域已经有 x,只是还没赋值,所以输出 undefined,不是全局的 10。
易错点:看调用位置判断作用域;忽略函数内后面的 var 声明;以为找不到才往上找,却忘了本层提升已经“占位”。
提升:var 提升与 function 提升
为什么需要:不理解提升,就会看不懂“声明写在后面,前面却能访问”的现象。
怎么想:提升发生在各自作用域里。全局中,var 会提前创建变量但不赋值;function 会提前创建变量并且值就是函数本身,所以感觉提升得更彻底。正式执行到 var 语句时赋值;正式执行到 function 声明语句时直接跳过。函数内也一样:调用时先预处理提升,再正式执行。
怎么做:
js
console.log(a); // undefined
var a = 1;
console.log(fn); // function fn() {}
function fn() {}易错点:把“提升”写成“提示”;以为 var 提升后值也有了;忽略函数内 var 声明会影响作用域链查找。
匿名函数与立即调用函数 IIFE
为什么需要:匿名函数就是函数直接量,常配合“立即调用”或“回调”使用。
怎么想:大多数函数还是希望起名字反复使用;但 IIFE 的目的不是多次使用,而是写完立即调用,产生一个独立作用域,避免全局变量污染。以后写 DOM 案例时,一个页面分多个模块,可以用 IIFE 做划分,变量重名也不怕。
怎么做:
js
(function () {
var temp = 1;
console.log(temp);
})();易错点:以为匿名函数不能调用;忘记 IIFE 外层小括号;把 IIFE 当成“为了复用”的函数。
回调函数与递归函数
为什么需要:回调函数非常常用,存在感很强;递归用于自己调用自己解决层层嵌套的问题。
怎么想:回调函数可以记成“我定义的、我没有直接调用、最终执行了”。它作为实参传给 forEach、sort 等函数,由内部调用;接收回调函数当参数或返回函数的函数叫高阶函数。递归必须有不再递归的条件,并且随着递归次数增加要趋向结束。
怎么做:
js
[1, 2, 3].forEach(function (item) {
console.log(item);
});
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}易错点:回调函数自己乱加小括号;递归没有终止条件;递归层次多时理不清执行顺序。复习优先级上,回调函数要彻底掌握,递归先理解清楚。
课堂画面






代码示例
示例1:用 sort 对“对象数组”按年龄排序
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>sort 数组排序</title>
</head>
<body>
<script>
// 数组的元素都是 object 对象:姓名、年龄、地址
var users = [
{ name: '张三', age: 37, address: '北京' },
{ name: '李四', age: 46, address: '上海' },
{ name: '王五', age: 47, address: '广州' },
{ name: '赵六', age: 51, address: '深圳' },
{ name: '孙七', age: 52, address: '杭州' },
{ name: '周八', age: 20, address: '成都' }
];
console.log(users);
// 必须传回调函数,否则默认按字符串方式排序
users.sort(function (next, prev) {
console.log(prev); // 打印出来是 object 对象
return next.age - prev.age; // 按年龄升序
});
console.log(users);
</script>
</body>
</html>关键行与易错点
users.sort()不传回调函数时,会把元素转成字符串比较,对对象数组毫无意义。- 回调函数形参的顺序是反的:第一个
next是后一个元素,第二个prev是前一个元素,写反了排序结果就颠倒。 - 两个对象不能直接比大小,必须取对象里的属性来比:
next.age - prev.age。 - 返回负数说明“后面的比前面的小”,
sort就会交换两者位置,于是得到升序;要降序就写成prev.age - next.age。 sort会改变原数组,排序前后的console.log(users)输出是不一样的。
示例2:用 for...in 遍历 Object 对象的属性
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>遍历 Object 对象的属性</title>
</head>
<body>
<script>
var obj = {
'user name': '张三', // 属性名不符合标识符规范,引号不能省
age: 18, // 符合标识符规范,引号可以省
'home-address': '北京',
'get in': true
};
// 对象没有 length、没有索引,不能用普通 for 循环遍历
for (var attr in obj) {
console.log(attr); // 属性名:user name / age / home-address / get in
console.log(obj[attr]); // 属性值
}
</script>
</body>
</html>关键行与易错点
for...in专门用来遍历对象:对象里有几个属性,循环体就执行几次。attr拿到的是属性名(字符串),不是属性值,这是最容易看错的一点。- 想拿属性值必须写
obj[attr]:因为attr是变量,中括号里不能加引号;一旦加上引号obj['attr'],就变成去找名字叫attr的属性了。 - 写成
obj.attr是错的:点语法会把attr当成固定的属性名,去对象里找“attr”这个属性,当然找不到(得到undefined)。 - 对象属性没有顺序,遍历出来的先后次序不必依赖。
示例3:delete 删除属性、in 判断属性是否存在
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>删除与判断对象属性</title>
</head>
<body>
<script>
var user = {
'user name': '张三',
age: 18,
'home-address': '北京',
'get in': true
};
console.log(user); // 删除前
// delete 是一元运算符,直接改变对象结构
delete user['user name']; // 属性名不符合标识符规范,只能用中括号
delete user['home-address'];
console.log(user); // 删除后少两个属性
// in 是二元运算符,表达式值是布尔值
console.log('get in' in user); // true
console.log('drink' in user); // false
// console.log(userName in user); // 报错:userName is not defined
</script>
</body>
</html>关键行与易错点
- 对象没有顺序,不能像数组那样按索引删,只能按属性名删:
delete 对象.属性名或delete 对象['属性名']。 delete是有副作用的一元运算符,执行完对象的属性就真的没了。- 属性名不符合标识符规范(如
user name、home-address)时,点语法不可用,只能写user['home-address']。 in运算符左边必须是属性名的字符串,所以'get in' in user要加引号。- 最典型的坑:
userName in user不加引号时,浏览器把userName当变量,变量未声明就直接报错;如果该变量已声明过,则会被当作“名为该变量值的属性”去判断,结果往往是false,同样不是你想要的效果。
常见错误
现象:对象数组用
sort()后年龄排序混乱。
原因:sort()默认按字符串比较,对象不能直接比大小。
修法:传比较器,按具体属性比较,如(next, prev) => next.age - prev.age。现象:
for...in中写obj.attr取不到属性值。
原因:attr是变量,点语法会把attr当成属性名。
修法:改用中括号且不加引号:obj[attr]。现象:
userName in user报变量未定义。
原因:左侧未加引号,被当作变量而不是属性名字符串。
修法:写成'userName' in user,或用变量保存属性名后再判断。现象:
delete user.home-address删除失败或写法异常。
原因:点语法只适合合法标识符属性名,home-address不符合。
修法:用中括号:delete user['home-address']。现象:读取不存在属性得到
undefined,误以为属性已添加。
原因:读取不存在属性返回undefined,赋值不存在属性才会新增。
修法:读取前先用in判断;新增属性用赋值操作。现象:用普通
for循环遍历对象属性失败。
原因:对象没有数组式length和连续索引。
修法:用for...in遍历属性名,再通过obj[key]取值。
练习
- 给定用户对象数组,按
age升序排序;若年龄相同,按name降序排序。 - 给定一个对象,用
for...in输出所有“属性名: 属性值”,遇到函数类型属性时跳过。 - 判断对象中是否存在
email属性;存在则删除,不存在则添加一个默认邮箱。 - 创建一个空对象,添加一个含连字符的属性名,完成读取、修改、删除操作。
- 用
in运算符判断对象中是否存在某个动态属性名,并说明属性名应如何传入。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 回顾 | 13:17 |
| 2 | 使用sort数组排序 | 04:33 |
| 3 | Object对象总结 | 06:38 |
| 4 | 遍历Object对象的属性 | 05:26 |
| 5 | 删除Object对象中的属性 | 02:52 |
| 6 | 判断Object对象中是否存在某个属性 | 07:33 |
小结
- 对象数组排序的关键不是“排序数组”,而是“排序对象中的某个可比属性”。
- 点语法与中括号语法的选择,核心看属性名是否合法、是否需要使用变量。
for...in拿到的是属性名,取属性值必须使用obj[属性名变量]。in表达式返回布尔值,左侧属性名不加引号时容易被误判为变量。delete是按属性名删除属性,特殊属性名优先使用中括号语法。- 对象读取不存在属性与给不存在属性赋值,结果完全不同,必须先区分场景。
- 回调函数仍是本阶段重点,
sort、forEach等案例要能独立分析执行过程。 - 递归理解成本较高,现阶段以能读懂、能说清终止条件为主。