学习笔记 2020-10-18
JavaScript 高级程序设计(第4版) 阅读记录
对象、类与面向对象编程
增强的对象语法
ES6
新增了很多极其有用的语法糖特性。
属性值简写
let name = 'Sirine'; let person = { name }; console.log(person); // {name: "Sirine"}
简写属性名只要使用变量名就会自动解释为同名的属性键,如果没有找到同名变量,则会抛出
ReferenceError
。可计算属性
以前,想使用变量的值作为属性,必须先声明对象,使用中括号语法来添加属性。
const nameKey = 'name'; const ageKey = 'age'; const jobKey = 'job'; let person = {}; person[nameKey] = 'Sirine'; person[ageKey] = 27; person[jobKey] = 'student'; console.log(person); // {name: "Sirine", age: 27, job: "student"}
可计算属性的用法:
const nameKey = 'name'; const ageKey = 'age'; const jobKey = 'job'; let person = { [nameKey]: 'Sirine', [ageKey]: 27, [jobKey]: 'student' }; console.log(person); // {name: "Sirine", age: 27, job: "student"}
可以在对象字面量中完成动态属性赋值。也可以使用复杂的表达式:
const nameKey = 'name'; const ageKey = 'age'; const jobKey = 'job'; let uniqueToken = 0; function getUniqueKey(key) { return `${key}_${uniqueToken++}`; } let person = { [getUniqueKey(nameKey)]: 'Sirine', [getUniqueKey(ageKey)]: 27, [getUniqueKey(jobKey)]: 'Student' }; console.log(person); // { name_0: 'Sirine', age_1: 27, job_2: 'Student' }
简写方法名
旧的方法定义方式:
let person = { sayName: function (name) { console.log(`My name is ${name}`); } }; person.sayName('Sirine'); // My name is Sirine
新的方法定义方式:
let person = { sayName(name) { console.log(`My name is ${name}`); } }; person.sayName('Sirine'); // My name is Sirine // 对获取函数和设置函数同样适用。 let person = { name_: '', get name() { return this.name_; }, set name(name) { this.name_ = name; }, sayName() { console.log(`My name is ${this.name_}`); } }; person.name = 'Sirine'; person.sayName(); // My name is Sirine
同时,简写方法名与可计算属性键相互兼容
对象解构
// 不使用对象解构
let person = {
name: 'Sirine',
age: 27
};
let personName = person.name,
personAge = person.age;
console.log(personName); // Sirine
console.log(personAge); // 27
// 使用对象解构
let person = {
name: 'Sirine',
age: 27
};
let { name: personName, age: personAge } = person;
console.log(personName); // Sirine
console.log(personAge); // 27
也可以使用简写语法。
let person = {
name: 'Sirine',
age: 27
};
let { name, age } = person;
console.log(name); // Sirine
console.log(age); // 27
若引用的属性不存在,该变量的值就是 undefined
。
也可以自定义默认值。
let person = {
name: 'Sirine',
age: 27
};
let { name, job = 'Student' } = person;
console.log(name); // Sirine
console.log(job); // Student
解构在内部使用函数 ToObject()
把源数据结构转换为对象。这意味着,原始值会被当成对象。 null
和 undefined
不能解构。否则会抛出 TypeError
。
如果是给事先声明的变量赋值,则赋值表达式需要包含在一对括号中。
let personName, personAge;
let person = {
name: 'Sirine',
age: 27
};
({ name: personName, age: personAge } = person);
console.log(personName, personAge); // Sirine, 27
嵌套解构
let person = { name: 'Sirine', age: 27, job: { title: 'Student' } }; let personCopy = {}; ({ name: personCopy.name, age: personCopy.age, job: personCopy.job } = person); // 因为一个对象的引用被赋值给 personCopy,所以修改 // person.job 对象的属性也会影响 personCopy person.age = 21; person.job.title = 'Hacker'; console.log(person); // { name: 'Sirine', age: 21, job: { title: 'Hacker' } } console.log(personCopy); // { name: 'Sirine', age: 27, job: { title: 'Hacker' } }
let person = { name: 'Sirine', age: 27, job: { title: 'Student' } }; let { job: { title } } = person; console.log(title); // Student
外层属性没有定义的情况下不能使用嵌套解构:
let person = { job: { title: 'Student' } }; 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
部分解构
涉及多个属性的解构赋值是一个输出无关的顺序化操作。如果开始赋值成功而后面赋值出错,解构赋值只能完成一部分。
let person = { name: 'Sirine', 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); // Sirine, undefined, undefined
参数上下文匹配
在函数参数列表中可以进行解构赋值,不会影响
arguments
对象。let person = { name: 'Sirine', 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: 'Sirine', age: 27 }, '2nd'] // 'Sirine', 27 printPerson2('1st', person, '2nd'); // ['1st', { name: 'Sirine', age: 27 }, '2nd'] // 'Sirine', 27
工厂模式
工厂模式是一种众所周知的设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。
function createPerson(name, age, job) {
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function () {
console.log(this.name);
};
return o;
}
let person1 = createPerson('Sirine', 29, 'Student');
let person2 = createPerson('Severus', 27, 'Teacher');
此处,可以通过多次调用不同参数的 createPerson
来创建不同的对象。但没有解决对象标识问题,即新创建的对象是什么类型。在这个例子中,创建的对象全部为 object
类型。
构造函数模式
在 js
中,我们可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
console.log(this.name);
};
}
let person1 = new Person('Sirine', 29, 'Student');
let person2 = new Person('Severus', 27, 'Teacher');
person1.sayName(); // Sirine
person2.sayName(); // Severus
console.log(person1.constructor == Person); // true
console.log(person2.constructor == Person); // true
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true
构造函数和工厂函数的区别是:
- 没有显式地创建对象。
- 属性和方法直接赋值给了
this
。 - 没有
return
。 - 函数名首字母大写。借鉴于面向对象编程语言。
创建实例使用 new
操作符。会执行以下操作:
- 在内存中创建一个新对象。
- 这个新对象内部的
[[Prototype]]
特性被赋值为构造函数的prototype
属性。 - 构造函数内部的
this
被赋值为这个新对象。 - 执行构造函数内部的代码。
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
定义自定义构造函数可以确保实例被标识为特定类型。
构造函数也可以是被赋值给变量的函数表达式。
在实例化时,构造函数后的括号不加也可以。只要有 new
操作符,就可以调用相应的构造函数。
构造函数也是函数
任何函数只要使用
new
操作符调用就是构造函数,不使用new
操作符调用就是普通函数。构造函数的问题
构造函数的问题是定义的方法会在每个实例上创建一遍。每个实例都会有自己的方法,而这些方法实际上是一样的,却指向不同的引用,这是没有必要的。如果我们选择把方法抽离到全局,让每个实例的方法都指向这个全局方法,可以保证不会重复声明函数,但这样污染了全局作用域,当构造函数的方法较多时,全局上就会多出很多不必要的函数,这些函数明明只是属于我们的实例对象,却定义在了全局。
CSS 知识点
又回顾到了浮动与清除浮动的知识,对于 clearfix
这种清除方法不是特别理解。自己写了写来尝试加深理解。
首先查阅了 MDN
关于 clear
的文档: 👉传送门
clear
CSS 属性指定一个元素是否必须移动(清除浮动后)到在它之前的浮动元素下面。clear
属性适用于浮动和非浮动元素。
文档中指出, clear
属性同时适用于浮动与非浮动元素。
这个特性给了解决浮动导致父元素高度塌陷的方法。
当父元素内的所有子元素都浮动,导致父元素内部高度无法被自动撑开。
此时,我们可以给父元素内部最后添加一个元素(经验证,此元素需要为块级元素),然后设置 clear: both
,表示当左右有浮动元素时需要移动到下面。该伪元素会自动移动到左右浮动元素最长的那个底下。
于是,父元素内部有了一个块级元素,且有位置要求,父元素就被撑开了。
为了简便,可以直接将该元素定义为父元素的伪元素 :after
。
且文档中提到一点,移动元素的垂直外边距会被折叠。
简单验证了一下,该元素的顶部外边距会被折叠,但底部外边距不会。
<div class="container clearfix">
<div class="left">left container</div>
<div class="right">right container</div>
<div class="bottom">to clearfix</div>
<div class="right">another right container</div>
</div>
.container {
border: 1px solid #000;
width: 800px;
}
.left {
background-color: red;
float: left;
height: 100px;
}
.right {
background-color: green;
float: right;
height: 200px;
}
.bottom {
margin-top: 50px;
margin-bottom: 50px;
border: 1px solid blue;
display: block;
clear: left;
}
另外,测试了一下浮动元素应用 clear
属性的效果。
<!DOCTYPE html>
<html lang="en">
<body>
<div class="container clearfix">
<div class="left">left container</div>
<div class="right">right container</div>
<div class="bottom">to clearfix</div>
<div class="left">another left container</div>
</div>
</body>
</html>
.container {
border: 1px solid #000;
width: 800px;
}
.left {
background-color: red;
float: left;
height: 100px;
}
.right {
background-color: green;
float: right;
height: 200px;
}
.bottom {
float: left;
border: 1px solid blue;
display: block;
clear: right;
}
可以看出来,浮动元素清除浮动后会影响到后续的元素,后续的元素只会跟着它进行排列。不会高于它。
将 .bottom
的 clear
修改为 left
。
将底部的最后那个元素修改为右浮动。