主题
Object 对象(Day7 3/3)
本页对应 Day07_JavaScript,共 6 段视频,约 1 小时 27 分。
学习目标
- 能说明递归函数的定义及递归成功条件,并判断给定递归函数是否会终止。
- 能分析含递归调用的函数执行过程,解释“挂起—返回”顺序,并预测阶乘函数及递归输出案例的结果。
- 能区分广义对象与狭义 Object 对象,说明 Object 是无序集合、由属性名和属性值组成,并识别“方法”的属性形式。
- 能使用直接量、Object 函数、new Object 三种方式创建对象,并完成属性的读取、修改、新增及不存在属性的结果判断。
- 能根据属性名是否符标识符规范、是否使用变量表示属性名,正确选择点语法或中括号语法读写对象属性。
知识点
先分清广义对象与狭义 Object
学习 Object 前,先要解决一个命名上的坑:JS 里“对象”有两个层面。广义理解是“一切皆对象”,数组、函数都是对象类型的一种;狭义理解则专指 Object 数据类型,它和 Array、Function 是平级的。为什么需要先分清?因为后面交流时说“对象”,可能指大类,也可能指 Object 这个小类。怎么想?看语境:如果它和数组、函数并列说,多半是狭义 Object;如果它和原始类型对立说,多半是广义对象。怎么做?本节先聚焦狭义 Object:它是一种无序集合,成员叫属性,属性由属性名和属性值组成;属性值可以是任意类型,若属性值是函数,这个属性通常称为方法。易错点:不要把“数组是有序集合”套到 Object 上,Object 的属性没有数组那样的 0、1、2 顺序。
创建 Object 的三种方式
为什么需要三种方式?因为要和数组的创建方式对照:直接量、Object 函数、Object 构造函数。怎么想?直接量最常用,能在创建时直接写属性;Object() 和 new Object() 更偏向创建空对象,之后再补属性。怎么做?可以这样写:
js
var obj1 = {}; // 空对象
var user = {
username: "高小乐",
age: 67,
friends: ["刘姥姥", "欧阳姥姥", "司马姥姥"],
child: {
name: "高小小乐",
age: 12
},
getInfo: function () {
console.log("高小乐爱学习");
}
};
var obj3 = Object();
var obj4 = new Object();
obj3.name = "张三";
obj4.age = 18;调试时输出 console.log(user),展开后能看到属性名和属性值;属性值是数组或对象时还能继续展开。易错点:Object() 和 new Object() 不能在创建时像直接量那样直接写属性,通常先创建空对象再赋值;对象字面量里属性之间要用逗号分隔。
属性名引号与点语法
为什么属性名要讨论引号?因为属性名本质上是字符串标识。怎么想?如果属性名符合标识符规范,也就是类似变量名的规则:字母、数字、下划线、$,且不能以数字开头、不是关键字,就可以省略引号;否则必须加引号。怎么做?
js
var user = {
username: "高小乐",
"home-address": "北京",
age: 67
};username、age 可以省略引号;home-address 不符合标识符规范,必须写成字符串。读取属性常用点语法:user.username、user.age。易错点:user.home-address 会被理解成 user.home - address,不是读取 "home-address" 属性,所以这种属性名不能用点语法,必须用中括号:user["home-address"]。
读写的规则与中括号语法
为什么还要中括号语法?因为点语法有局限:属性名不符合标识符规范时不能用;属性名由变量表示时也不能直接用点语法。怎么想?点语法后面写的是“属性名本身”;中括号里如果不加引号,写的是“变量”,变量的值才是属性名。怎么做?
js
console.log(user.username); // 高小乐
console.log(user["home-address"]); // 北京
console.log(user.job); // undefined
user.age = 77;
user.phone = "13800000000";
console.log(user.age); // 77
console.log(user.phone); // 13800000000
var prop = "friends";
console.log(user[prop]); // 读取 friends 属性读取不存在的属性会得到 undefined;给不存在的属性赋值会添加该属性。调用不存在的方法会报错,因为它本质上是先得到 undefined,再对 undefined 加括号调用。易错点:中括号里写变量时不要加引号,user[prop] 和 user["prop"] 含义不同;点语法不能写成 user.prop 来读取变量 prop 对应的属性,因为那会被当成名为 prop 的属性。
作业一:用 sort 按年龄升序排列对象数组
为什么需要users 不是普通数字数组,而是“对象数组”。如果直接写 users.sort(),sort 默认会把元素转成字符串再比较,得到的顺序不一定符合“年龄升序”。所以这里需要给 sort 传一个比较函数,明确告诉它:按 age 这个属性比大小。转写稿里讲对象时提到,对象由属性名和属性值组成,读取属性常用点语法,例如 user.age。排序时正是要读每个对象的 age。
怎么想sort 的比较函数接收两个参数,通常写成 a、b。返回值小于 0,表示 a 排在 b 前面;大于 0 表示 a 排在 b 后面;等于 0 表示保持相对顺序。年龄升序就是“小的在前”,所以可以返回 a.age - b.age。这样写比手写 if 更直接。
怎么做
先保留题目数据:
js
// 1. 数组排序 sort 方法 按照年龄升序排列
var users = [
{name: '曹操', age: 67, address: '许昌'},
{name: '刘备', age: 47, address: '成都'},
{name: '孙权', age: 37, address: '南京'},
{name: '吕布', age: 51, address: '包头'},
{name: '赵云', age: 46, address: '石家庄'},
{name: '诸葛亮', age: 52, address: '南阳'}
]然后补上排序和验证:
js
users.sort(function (a, b) {
return a.age - b.age;
});
console.log(users);排序后应能看到孙权 37、赵云 46、刘备 47、吕布 51、诸葛亮 52、曹操 67。实际调试时不要只看“代码没报错”,要把结果打印出来,逐项检查年龄是否从小到大。
易错点
第一,不传比较函数,误以为 sort 会自动按数字排。第二,比较函数返回布尔值,例如 return a.age > b.age,这不符合 sort 对正负数的要求。第三,忘记 sort 会修改原数组;如果还要保留原顺序,应先复制再排序。第四,读取属性时写错属性名,例如把 age 写成 Age,会得到 undefined,比较结果就不对。转写稿里也提醒过:读取不存在的属性会得到 undefined,给不存在的属性赋值则会添加该属性,所以属性名要仔细核对。
作业二:用递归函数拉平数组(选做)
为什么需要
题目里的 nums 是多层嵌套数组,里面有数字、字符串,也有更深的数组。如果只用普通循环,很难提前知道要写几层循环,因为结构是发散的。转写稿明确说,这类“拉平数组”的作业就是提前感受递归的必要性;递归函数是“函数体内自己调用自己”,适合处理这种层级不确定的结构。
怎么想
先准备一个结果数组 result,然后遍历当前数组的每一项:如果这一项还是数组,就继续交给递归处理;如果不是数组,就把它放进 result。递归的结束条件不是“某个数字等于 2”,而是“当前这一层遍历完”。转写稿讲递归成功条件时强调:要有明确的结束递归条件,并且随着递归调用次数增加,条件要趋向于结束递归。拉平数组里,每进入一层子数组,层级就在减少,最终会遍历完。
怎么做
原笔记代码里有一个中文逗号,实际运行要改成英文逗号。先保留原题:
js
// 2. 拉平数组 一维数组 递归函数 (选做)
var nums = [
[1000, 2000, 3000],
'hello',
[
[10,20,30],
[
'a',
'b',
['A', 'B', 'C']
],
'小乐'
],
12313,
[101,202,303]
]
// 改为
[1000,2000,3000,'hello', 10,20,30,'a','b','A','B','C','小乐',12313,101,202,303]修正并实现:
js
var nums = [
[1000, 2000, 3000],
'hello',
[
[10, 20, 30],
[
'a',
'b',
['A', 'B', 'C']
],
'小乐'
],
12313,
[101, 202, 303]
];
function flatten(arr) {
var result = [];
for (var i = 0; i < arr.length; i++) {
var item = arr[i];
if (Array.isArray(item)) {
result = result.concat(flatten(item));
} else {
result.push(item);
}
}
return result;
}
console.log(flatten(nums));输出应为:
js
[1000, 2000, 3000, 'hello', 10, 20, 30, 'a', 'b', 'A', 'B', 'C', '小乐', 12313, 101, 202, 303]易错点
第一,中文逗号会导致语法错误,必须改成英文逗号。第二,判断“是不是数组”不能只靠 typeof,因为 typeof [] 也是 "object",这里应使用 Array.isArray(item)。第三,递归调用后忘记把结果合并回 result,或者忘记 return result。第四,把字符串误当成数组处理;字符串不能被 Array.isArray 识别为数组,应原样放入结果。第五,转写稿提醒过,递归调用会“挂起”,要等内部递归结束再继续执行;理解这一点,才能明白为什么每一层子数组处理完后还能继续合并结果。递归不如循环直观,能循环解决时优先循环;但像这种多层发散结构,递归更合适。
课堂画面






代码示例
第 1 段:递归函数 —— 阶乘 + 挂起后继续执行
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>递归函数</title>
</head>
<body>
<script>
// 1. 用递归求 n 的阶乘
function factorial(n) {
// 结束递归的条件:n <= 2 时不需要再往下算
if (n <= 2) {
return n; // 2 的阶乘是 2,1 的阶乘是 1
}
// n 的阶乘 = n * (n-1) 的阶乘 —— 这里就是递归调用
return n * factorial(n - 1);
}
console.log(factorial(10)); // 3628800
console.log(factorial(5)); // 120
console.log(factorial(8)); // 40320
// 2. 递归调用被挂起后还会继续往下执行
function func(n) {
console.log(n); // 第一次输出
if (n > 1) {
func(n - 1); // 递归调用:本次调用被挂起
}
console.log(n); // 挂起结束、递归返回后继续执行第二次输出
}
func(3); // 321123
</script>
</body>
</html>要点解释与易错点:
factorial中n * factorial(n - 1)是关键行:5 的阶乘 = 5 × 4 的阶乘,一路递归到 2 就结束。- 递归必须有两个条件:明确的结束递归条件(
n <= 2)+ 每次调用都趋向于结束条件(n - 1)。转写稿中那个function func(){ console.log('被调用'); func(); }没有结束条件,浏览器会因超出最大调用次数而报错。 - 递归执行顺序像俄罗斯套娃:最先调用的最后结束,最后调用的最先结束。5 的调用挂起等 4,4 挂起等 3,3 挂起等 2,2 直接返回后依次回算 3×2=6、4×6=24、5×24=120。
func(3)输出321123的易错点:里面没有return,递归调用只是把当前调用“挂起”,不是结束函数,所以func(n - 1)返回后下面的console.log(n)还会再执行一次。若写成return func(n - 1);就只输出321。
第 2 段:递归拉平数组(作业题)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>拉平数组</title>
</head>
<body>
<script>
var nums = [
[1000, 2000, 3000],
'hello',
[
[10, 20, 30],
['a', 'b', ['A', 'B', 'C']],
'小乐'
],
12313,
[101, 202, 303]
];
var result = [];
function flatten(arr) {
for (var i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
// 成员还是数组,继续递归往里拆
flatten(arr[i]);
} else {
// 成员不是数组,直接收集
result.push(arr[i]);
}
}
return result;
}
console.log(flatten(nums));
// [1000,2000,3000,'hello',10,20,30,'a','b','A','B','C','小乐',12313,101,202,303]
</script>
</body>
</html>要点解释与易错点:
- 关键行是
Array.isArray(arr[i])判断:是数组就递归拆,不是数组就push进结果。 - 这种结构“发散”、层数不确定,用普通 for 循环无法固定嵌套层数,所以才必须用递归 —— 这正是转写稿说的“循环搞不定的事才用递归”。
- 易错点 1:
result必须定义在函数外面(或用闭包),否则每次递归都会重置,最后只剩一层数据。 - 易错点 2:
flatten(arr[i])里面不要再写return,否则第一层拆完就直接结束了,后面的元素收集不到。
第 3 段:Object 对象的创建与属性读写
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Object 对象</title>
</head>
<body>
<script>
// 直接量方式创建对象(最常用)
var user = {
userName: '高老正',
age: 67,
'home-address': '北京', // 不符合标识符规范,必须加引号
schoolAddress: '石家庄',
friends: ['刘姥姥', '欧阳姥姥', '司马姥姥'],
child: {
name: '高小乐',
age: 37
},
getInfo: function () { // 属性值是函数 —— 称为“方法”
console.log('高小乐爱吃');
}
};
// 读:点语法
console.log(user.userName); // 高老正
console.log(user.age * 6); // 参与运算
console.log(user.getInfo); // 输出函数本身
user.getInfo(); // 加括号才是调用
console.log(user.friends[2]); // 司马姥姥
console.log(user.child.name); // 高小乐
// 读不存在的属性 → undefined;调用不存在的方法 → 报错
console.log(user.job); // undefined
// user.job(); // TypeError: user.job is not a function
// 写:修改已有属性、给不存在的属性赋值(会自动添加)
user.age += 10;
user.phone = '13800000000';
console.log(user);
// 中括号语法:属性名必须加引号,否则会被当成变量
console.log(user['userName']);
console.log(user['home-address']); // 含杠的属性名必须用中括号
user['home-address'] = '许昌';
// 用变量表示属性名时,中括号里不能加引号
var prop = 'friends';
console.log(user[prop]); // 拿到数组
// console.log(user.prop); // undefined —— 点语法把 prop 当成了属性名
// 另外两种创建方式:只能创建空对象,属性要事后添加
var obj1 = Object();
var obj2 = new Object();
obj1.name = '张三';
obj2.name = '李四';
console.log(obj1, obj2);
</script>
</body>
</html>要点解释与易错点:
- 属性由属性名 + 属性值组成,值可以是任意类型(字符串、数字、数组、对象、函数)。
- 属性名是字符串:符合标识符规范(变量命名规范)时可省略引号,
'home-address'含-必须加引号,否则会被解析成减法。 - 点语法必须用于合法属性名;含特殊字符的属性名和用变量表示属性名这两种情况必须用中括号语法,且前者加引号、后者不加引号。
user.getInfo是函数本身,user.getInfo()才是调用;读取不存在的属性得到undefined,在undefined后加括号调用就会报错 —— 本质是同一回事,不用分开记。- 给不存在的属性赋值会自动添加该属性(和数组给不存在的索引赋值一样),所以
Object()/new Object()创建的空对象可以后续用点语法补属性。
常见错误
现象: 递归函数运行后报
Maximum call stack size exceeded。原因: 缺少终止条件,或递归参数没有向终止条件收敛。修法: 先写基线return,再写递归关系,并检查每次调用参数是否必然接近基线。现象: 阶乘函数
factorial(1)得到2。原因: 把基线写成n <= 2时直接返回固定值2,没有区分1和2。修法: 使用if (n <= 1) return 1,或分别处理n === 1、n === 2。现象:
users.sort()后年龄顺序仍不符合数值升序。原因:sort默认按字符串规则排序,未传比较函数。修法: 写users.sort((a, b) => a.age - b.age);若不想改原数组,先复制:[...users].sort(...)。现象: 拉平数组后结果里仍残留嵌套数组。原因: 只处理了第一层,或递归时没有区分“数组元素”和“普通元素”。修法: 用
Array.isArray(item)分支:数组继续递归,非数组直接放入结果,并保持原顺序。现象:
user.home-address报错或结果异常。原因: 点语法要求属性名符合标识符规范,home-address中的-被当成减号。修法: 改用user['home-address'];若属性名存放在变量中,用user[key],不要写成user.key。现象: 读取不存在属性得到
undefined后继续加括号调用,程序报TypeError。原因:undefined不是函数,不能调用。修法: 调用前判断typeof obj.fn === 'function',或先确认属性存在。
练习
- 将
users按age升序排列,要求不修改原数组,并输出姓名顺序。 - 写递归函数
factorial(n),要求factorial(0)、factorial(1)都返回1,并说明factorial(5)的返回顺序。 - 用递归实现
flatten(nums),把笔记中的多维数组拉平为一维数组,要求保持元素原顺序。 - 给定对象
user = { name: '曹操', 'home-address': '许昌', friends: ['刘备'], getInfo(){} },分别读取home-address、friends[0],并用变量key = 'name'读取姓名。 - 判断输出:
function f(n){ console.log(n); if(n > 1) f(n - 1); console.log(n); } f(3);写出输出结果并说明原因。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 递归函数 | 32:44 |
| 2 | 递归应用 | 04:35 |
| 3 | 对象的概念 | 09:40 |
| 4 | 什么是Object对象 | 03:29 |
| 5 | 如何创建一个Object对象 | 14:05 |
| 6 | 读写Object对象的属性 | 22:42 |
小结
- 递归函数必须先明确终止条件,再保证参数逐步靠近终止条件。
- 递归调用会挂起当前调用;递归返回后,挂起处后面的代码仍会继续执行。
- 能用循环清晰解决的问题优先用循环;递归更适合处理嵌套、发散结构。
sort默认不是按数值排序;对象字段排序必须传比较函数,并注意它是否修改原数组。- 对象属性读写优先点语法,但属性名不符合标识符规范或用变量作键时必须用中括号。
- 读取不存在属性得到
undefined;给不存在属性赋值会新增属性;对undefined加括号调用会报错。 - 拉平数组的关键是
Array.isArray分支与顺序累积,递归和迭代栈都可实现。 - 对象属性值可为任意类型;属性值为函数时称为方法,但本质仍是属性。