Skip to content

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); // 3

return 负责设置返回值;没有写 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

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例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,同样不是你想要的效果。

常见错误 ​

  1. 现象:对象数组用 sort() 后年龄排序混乱。
    原因:sort() 默认按字符串比较,对象不能直接比大小。
    修法:传比较器,按具体属性比较,如 (next, prev) => next.age - prev.age。

  2. 现象:for...in 中写 obj.attr 取不到属性值。
    原因:attr 是变量,点语法会把 attr 当成属性名。
    修法:改用中括号且不加引号:obj[attr]。

  3. 现象:userName in user 报变量未定义。
    原因:左侧未加引号,被当作变量而不是属性名字符串。
    修法:写成 'userName' in user,或用变量保存属性名后再判断。

  4. 现象:delete user.home-address 删除失败或写法异常。
    原因:点语法只适合合法标识符属性名,home-address 不符合。
    修法:用中括号:delete user['home-address']。

  5. 现象:读取不存在属性得到 undefined,误以为属性已添加。
    原因:读取不存在属性返回 undefined,赋值不存在属性才会新增。
    修法:读取前先用 in 判断;新增属性用赋值操作。

  6. 现象:用普通 for 循环遍历对象属性失败。
    原因:对象没有数组式 length 和连续索引。
    修法:用 for...in 遍历属性名,再通过 obj[key] 取值。

练习 ​

  1. 给定用户对象数组,按 age 升序排序;若年龄相同,按 name 降序排序。
  2. 给定一个对象,用 for...in 输出所有“属性名: 属性值”,遇到函数类型属性时跳过。
  3. 判断对象中是否存在 email 属性;存在则删除,不存在则添加一个默认邮箱。
  4. 创建一个空对象,添加一个含连字符的属性名,完成读取、修改、删除操作。
  5. 用 in 运算符判断对象中是否存在某个动态属性名,并说明属性名应如何传入。

原视频对照 ​

#视频时长
1回顾13:17
2使用sort数组排序04:33
3Object对象总结06:38
4遍历Object对象的属性05:26
5删除Object对象中的属性02:52
6判断Object对象中是否存在某个属性07:33

小结 ​

  1. 对象数组排序的关键不是“排序数组”,而是“排序对象中的某个可比属性”。
  2. 点语法与中括号语法的选择,核心看属性名是否合法、是否需要使用变量。
  3. for...in 拿到的是属性名,取属性值必须使用 obj[属性名变量]。
  4. in 表达式返回布尔值,左侧属性名不加引号时容易被误判为变量。
  5. delete 是按属性名删除属性,特殊属性名优先使用中括号语法。
  6. 对象读取不存在属性与给不存在属性赋值,结果完全不同,必须先区分场景。
  7. 回调函数仍是本阶段重点,sort、forEach 等案例要能独立分析执行过程。
  8. 递归理解成本较高,现阶段以能读懂、能说清终止条件为主。