2025-01-12 日报 Day64

2025-01-12 日报 Day64

Yuyang 前端小白🥬

今日的鸡汤

这个世界上的大部分传奇,不过是普普通通的人们,将心意化作了行动而已。

今日学习内容

1、JS 红皮书 P213-219 第八章:对象、类与面向对象编程

今日笔记

1、对象标识及相等判定: 在 ECMAScript 6 之前,有些特殊情况即使是===操作符也无能为力:

1
2
3
4
5
6
7
8
9
10
11
// 这些是===符合预期的情况
console.log(true === 1); // false
console.log({} === {}); // false
console.log("2" === 2); // false
// 这些情况在不同 JavaScript 引擎中表现不同,但仍被认为相等
console.log(+0 === -0); // true
console.log(+0 === 0); // true
console.log(-0 === 0); // true
// 要确定 NaN 的相等性,必须使用极为讨厌的 isNaN()
console.log(NaN === NaN); // false
console.log(isNaN(NaN)); // true

为改善这类情况,ECMAScript 6 规范新增了 Object.is(),这个方法与===很像,但同时也考虑到了上述边界情形。这个方法必须接收两个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
console.log(Object.is(true, 1)); // false
console.log(Object.is({}, {})); // false
console.log(Object.is("2", 2)); // false
// 正确的 0、-0、+0 相等/不等判定
console.log(Object.is(+0, -0)); // false
console.log(Object.is(+0, 0)); // true
console.log(Object.is(-0, 0)); // false
// 正确的 NaN 相等判定
console.log(Object.is(NaN, NaN)); // true
// 要检查超过两个值,递归地利用相等性传递即可:
function recursivelyCheckEqual(x, ...rest) {
return (
Object.is(x, rest[0]) && (rest.length < 2 || recursivelyCheckEqual(...rest))
);
}

2、增强的对象语法: ECMAScript 6 为定义和操作对象新增了很多极其有用的语法糖特性。这些特性都没有改变现有引擎的行为,但极大地提升了处理对象的方便程度。

  • 属性值简写: 在给对象添加变量的时候,开发者经常会发现属性名和变量名是一样的。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let name = "Nicholas";
let person = {
name: name,
};
// 在 ECMAScript 6 中,可以简写成:
let name = "Matt";
let person = {
name,
};
console.log(person); // { name: 'Matt' }

function makePerson(name) {
return {
name,
};
}
let person = makePerson("Matt");
console.log(person.name); // Matt

function makePerson(a) {
return {
name: a,
};
}
var person = makePerson("Matt");
console.log(person.name); // Matt
  • 可计算属性: 在引入可计算属性之前,如果想使用变量的值作为属性,那么必须先声明对象,然后使用中括号语法来添加属性。换句话说,不能在对象字面量中直接动态命名属性。比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const nameKey = "name";
const ageKey = "age";
const jobKey = "job";
let person = {};
person[nameKey] = "Matt";
person[ageKey] = 27;
person[jobKey] = "Software engineer";
console.log(person); // { name: 'Matt', age: 27, job: 'Software engineer' }
// 有了可计算属性,就可以在对象字面量中完成动态属性赋值。中括号包围的对象属性键告诉运行时将其作为 JavaScript 表达式而不是字符串来求值:
const nameKey = "name";
const ageKey = "age";
const jobKey = "job";
let person = {
[nameKey]: "Matt",
[ageKey]: 27,
[jobKey]: "Software engineer",
};
console.log(person); // { name: 'Matt', age: 27, job: 'Software engineer' }
// 因为被当作 JavaScript 表达式求值,所以可计算属性本身可以是复杂的表达式,在实例化时再求值:
const nameKey = "name";
const ageKey = "age";
const jobKey = "job";
let uniqueToken = 0;
function getUniqueKey(key) {
return `${key}_${uniqueToken++}`;
}
let person = {
[getUniqueKey(nameKey)]: "Matt",
[getUniqueKey(ageKey)]: 27,
[getUniqueKey(jobKey)]: "Software engineer",
};
console.log(person); // { name_0: 'Matt', age_1: 27, job_2: 'Software engineer' }

注意 可计算属性表达式中抛出任何错误都会中断对象创建。如果计算属性的表达式有副作用,那就要小心了,因为如果表达式抛出错误,那么之前完成的计算是不能回滚的。

  • 简写方法名: 在给对象定义方法时,通常都要写一个方法名、冒号,然后再引用一个匿名函数表达式,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
let person = {
sayName: function (name) {
console.log(`My name is ${name}`);
},
};
person.sayName("Matt"); // My name is Matt

let person = {
sayName(name) {
console.log(`My name is ${name}`);
},
};
person.sayName("Matt"); // My name is Matt

简写方法名对获取函数和设置函数也是适用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let person = {
name_: "",
get name() {
return this.name_;
},
set name(name) {
this.name_ = name;
},
sayName() {
console.log(`My name is ${this.name_}`);
},
};
person.name = "Matt";
person.sayName(); // My name is Matt

简写方法名与可计算属性键相互兼容:

1
2
3
4
5
6
7
const methodKey = "sayName";
let person = {
[methodKey](name) {
console.log(`My name is ${name}`);
},
};
person.sayName("Matt"); // My name is Matt

3、对象解构: ECMAScript 6 新增了对象解构语法,可以在一条语句中使用嵌套数据实现一个或多个赋值操作。简单地说,对象解构就是使用与对象匹配的结构来实现对象属性赋值。
下面的例子展示了两段等价的代码,首先是不使用对象解构的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 不使用对象解构
let person = {
name: "Matt",
age: 27,
};
let personName = person.name,
personAge = person.age;
console.log(personName); // Matt
console.log(personAge); // 27
// 然后,是使用对象解构的:
// 使用对象解构
let person = {
name: "Matt",
age: 27,
};
let { name: personName, age: personAge } = person;
console.log(personName); // Matt
console.log(personAge); // 27

使用解构,可以在一个类似对象字面量的结构中,声明多个变量,同时执行多个赋值操作。如果想让变量直接使用属性的名称,那么可以使用简写语法,比如:

1
2
3
4
5
6
7
let person = { 
name: 'Matt',
age: 27
};
let { name, age } = person;
console.log(name); // Matt
console.log(age); // 27

解构赋值不一定与对象的属性匹配。赋值的时候可以忽略某些属性,而如果引用的属性不存在,则该变量的值就是 undefined:

1
2
3
4
5
6
7
let person = { 
name: 'Matt',
age: 27
};
let { name, job } = person;
console.log(name); // Matt
console.log(job); // undefined

也可以在解构赋值的同时定义默认值,这适用于前面刚提到的引用的属性不存在于源对象中的情况:

1
2
3
4
5
6
7
let person = { 
name: 'Matt',
age: 27
};
let { name, job='Software engineer' } = person;
console.log(name); // Matt
console.log(job); // Software engineer

解构在内部使用函数 ToObject()(不能在运行时环境中直接访问)把源数据结构转换为对象。这意味着在对象解构的上下文中,原始值会被当成对象。这也意味着(根据 ToObject()的定义),null和 undefined 不能被解构,否则会抛出错误。

1
2
3
4
5
6
let { length } = 'foobar'; 
console.log(length); // 6
let { constructor: c } = 4;
console.log(c === Number); // true
let { _ } = null; // TypeError
let { _ } = undefined; // TypeError

解构并不要求变量必须在解构表达式中声明。不过,如果是给事先声明的变量赋值,则赋值表达式必须包含在一对括号中:

1
2
3
4
5
6
7
let personName, personAge; 
let person = {
name: 'Matt',
age: 27
};
({name: personName, age: personAge} = person);
console.log(personName, personAge); // Matt, 27

3、嵌套解构: 解构对于引用嵌套的属性或赋值目标没有限制。为此,可以通过解构来复制对象属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let person = { 
name: 'Matt',
age: 27,
job: {
title: 'Software engineer'
}
};
let personCopy = {};
({ name: personCopy.name, age: personCopy.age, job: personCopy.job } = person);
// 因为一个对象的引用被赋值给 personCopy,所以修改
// person.job 对象的属性也会影响 personCopy
person.job.title = 'Hacker'

console.log(person);
// { name: 'Matt', age: 27, job: { title: 'Hacker' } }

console.log(personCopy);
// { name: 'Matt', age: 27, job: { title: 'Hacker' } }
// 解构赋值可以使用嵌套结构,以匹配嵌套的属性:
let person = {
name: 'Matt',
age: 27,
job: {
title: 'Software engineer'
}
};
// 声明 title 变量并将 person.job.title 的值赋给它
let { job: { title } } = person;
console.log(title); // Software engineer

在外层属性没有定义的情况下不能使用嵌套解构。无论源对象还是目标对象都一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let person = { 
job: {
title: 'Software engineer'
}
};
let personCopy = {};
// foo 在源对象上是 undefined
({
foo: {
bar: personCopy.bar
}
} = person);
// TypeError: Cannot destructure property 'bar' of 'undefined' or 'null'.
// job 在目标对象上是 undefined
({
job: {
title: personCopy.job.title
}
} = person);
// TypeError: Cannot set property 'title' of undefined
  • 部分解构: 需要注意的是,涉及多个属性的解构赋值是一个输出无关的顺序化操作。如果一个解构表达式涉及多个赋值,开始的赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分:
1
2
3
4
5
6
7
8
9
10
11
let person = { 
name: 'Matt',
age: 27
};
let personName, personBar, personAge;
try {
// person.foo 是 undefined,因此会抛出错误
({name: personName, foo: { bar: personBar }, age: personAge} = person);
} catch(e) {}
console.log(personName, personBar, personAge);
// Matt, undefined, undefined
  • 参数上下文匹配: 在函数参数列表中也可以进行解构赋值。对参数的解构赋值不会影响 arguments 对象,但可以在函数签名中声明在函数体内使用局部变量:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let person = { 
name: 'Matt',
age: 27
};
function printPerson(foo, {name, age}, bar) {
console.log(arguments);
console.log(name, age);
}
function printPerson2(foo, {name: personName, age: personAge}, bar) {
console.log(arguments);
console.log(personName, personAge);
}
printPerson('1st', person, '2nd');
// ['1st', { name: 'Matt', age: 27 }, '2nd']
// 'Matt', 27
printPerson2('1st', person, '2nd');
// ['1st', { name: 'Matt', age: 27 }, '2nd']
// 'Matt', 27
此页目录
2025-01-12 日报 Day64