学习笔记 2020-10-22
JavaScript 高级程序设计(第4版) 阅读记录
对象、类与面向对象编程
类
实例、原型和类成员
实例成员
原型方法与访问器
在类块中定义的方法作为原型方法。
可以把方法定义在类构造函数中或者类块中,但不能在类块中给原型添加原始值或对象作为成员数据。
类方法等同于对象属性,可以使用字符串、符号或计算的值作为键。
const symbolKey = Symbol('symbolKey'); class Person { stringKey() { console.log('invoked stringKey'); } [symbolKey]() { console.log('invoked symbolKey'); } ['computed' + 'Key']() { console.log('invoked computedKey'); } } let p = new Person(); p.stringKey(); // invoked stringKey p[symbolKey](); // invoked symbolKey p.computedKey(); // invoked computedKey
类定义也支持获取和设置访问器。
class Person { set name(newName) { this.name_ = newName; } get name() { return this.name_; } } let p = new Person(); p.name = 'Jake'; console.log(p.name); // Jake
静态类方法
静态类成员在类定义中使用
static
关键字作为前缀。在静态成员中,this
引用类自身。class Person { constructor() { // 添加到 this 的所有内容都会存在于不同的实例上 this.locate = () => console.log('instance', this); } // 定义在类的原型对象上 locate() { console.log('prototype', this); } // 定义在类本身上 static locate() { console.log('class', this); } static name = '123'; static age = 12; } let p = new Person(); p.locate(); // instance, Person {} Person.prototype.locate(); // prototype, {constructor: ... } Person.locate(); // class, class Person {} console.log(Person.name); console.log(Person.age);
静态类方法适合作为实例工厂:
class Person { constructor(age) { this.age_ = age; } sayAge() { console.log(this.age_); } static create() { // 使用随机年龄创建并返回一个 Person 实例 return new Person(Math.floor(Math.random() * 100)); } } console.log(Person.create()); // Person { age_: ... }
非函数原型和类成员
在类的外部,可以给原型手动添加属性。
迭代器与生成器语法
类定义语法支持在原型和类本身上定义生成器方法:
class Person { // 在原型上定义生成器方法 *createNicknameIterator() { yield 'Jack'; yield 'Jake'; yield 'J-Dog'; } // 在类上定义生成器方法 static *createJobIterator() { yield 'Butcher'; yield 'Baker'; yield 'Candlestick maker'; } } let jobIter = Person.createJobIterator(); console.log(jobIter.next().value); // Butcher console.log(jobIter.next().value); // Baker console.log(jobIter.next().value); // Candlestick maker let p = new Person(); let nicknameIter = p.createNicknameIterator(); console.log(nicknameIter.next().value); // Jack console.log(nicknameIter.next().value); // Jake console.log(nicknameIter.next().value); // J-Dog
也可以添加一个默认的迭代器,把类实例变成可迭代对象。
class Person { constructor() { this.nicknames = ['Jack', 'Jake', 'J-Dog']; } *[Symbol.iterator]() { yield* this.nicknames.entries(); } } let p = new Person(); for (let [idx, nickname] of p) { console.log(nickname); } // Jack // Jake // J-Dog
class Person { constructor() { this.nicknames = ['Jack', 'Jake', 'J-Dog']; } [Symbol.iterator]() { return this.nicknames.entries(); } } let p = new Person(); for (let [idx, nickname] of p) { console.log(nickname); } // Jack // Jake // J-Dog
继承
ES6
的类继承使用了新语法,但背后依然使用原型链。
继承基础
ES6
类支持单继承。使用extends
关键字,就可以继承任何拥有[[Construct]]
和原型的对象。不仅可以继承一个类,也可以继承普通的构造函数。class Vehicle {} // 继承类 class Bus extends Vehicle {} let b = new Bus(); console.log(b instanceof Bus); // true console.log(b instanceof Vehicle); // true function Person() {} // 继承普通构造函数 class Engineer extends Person {} let e = new Engineer(); console.log(e instanceof Engineer); // true console.log(e instanceof Person); // true
派生类可以通过原型链访问到类和原型上定义的方法。
extends
关键字也可以在类表达式中使用。构造函数、
HomeObject
和super()
在派生类的构造函数中调用
super
来调用父类构造函数。也可以通过super
来调用继承的类上定义的静态方法。class Vehicle { static name = '123'; static identify() { console.log('vehicle'); } } class Bus extends Vehicle { static identify() { super.identify(); console.log(super.name); // 123 } } Bus.identify(); // vehicle
ES6
给类构造函数和静态方法添加了内部特性[[HomeObject]]
,这个特性是一个指针,指向定义该方法的对象。这个指针是自动赋值的,只能在JavaScript
引擎内部访问。super
始终会定义为[[HomeObject]]
的原型。super
只能在派生类构造函数和静态方法中使用。- 不能单独引用
super
关键字,要么用它调用构造函数,要么用它引用静态方法。 - 调用
super()
会调用父类构造函数,并将返回的实例赋值给this
。 super()
的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入。- 如果没有定义类构造函数,在实例化派生类时会调用
super()
,而且会传入所有传给派生类的参数。 - 在类构造函数中,不能在调用
super()
之前引用this
。 - 如果派生类显式定义了构造函数,则必须调用
super()
,要么必须在其中返回一个对象。
抽象基类
抽象基类是指可被其他类继承但自身不会被实例化的类。
ECMAScript
中没有专门支持这种类的语法。但可以通过new.target
来实现。new.target
保存通过new
关键字调用的类或函数。通过在实例化时检查new.target
是不是抽象基类,可以阻止对抽象基类的实例化。// 抽象基类 class Vehicle { constructor() { console.log(new.target); if (new.target === Vehicle) { throw new Error('Vehicle cannot be directly instantiated'); } } } // 派生类 class Bus extends Vehicle {} new Bus(); // class Bus {} new Vehicle(); // class Vehicle {} // Error: Vehicle cannot be directly instantiated
可以通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。
// 抽象基类 class Vehicle { constructor() { if (new.target === Vehicle) { throw new Error('Vehicle cannot be directly instantiated'); } if (!this.foo) { throw new Error('Inheriting class must define foo()'); } console.log('success!'); } } // 派生类 class Bus extends Vehicle { foo() {} } // 派生类 class Van extends Vehicle {} new Bus(); // success! new Van(); // Error: Inheriting class must define foo()
此处的原理是原型函数在调用类构造函数之前就已经存在,以此来进行判断。
继承内置类型
ES6
类可以方便地拓展内置类型。class SuperArray extends Array { shuffle() { // 洗牌算法 for (let i = this.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [this[i], this[j]] = [this[j], this[i]]; } } } let a = new SuperArray(1, 2, 3, 4, 5); console.log(a instanceof Array); // true console.log(a instanceof SuperArray); // true console.log(a); // [1, 2, 3, 4, 5] a.shuffle(); console.log(a); // [3, 1, 4, 5, 2]
内置类型的方法返回了新的实例,该实例类型依然保持不变。
class SuperArray extends Array {} let a1 = new SuperArray(1, 2, 3, 4, 5); let a2 = a1.filter(x => !!(x % 2)); console.log(a1); // [1, 2, 3, 4, 5] console.log(a2); // [1, 3, 5] console.log(a1 instanceof SuperArray); // true console.log(a2 instanceof SuperArray); // true
可以通过覆盖
Symbol.species
访问器来覆盖这个默认行为。class SuperArray extends Array { static get [Symbol.species]() { return Array; } } let a1 = new SuperArray(1, 2, 3, 4, 5); let a2 = a1.filter(x => !!(x % 2)); console.log(a1); // [1, 2, 3, 4, 5] console.log(a2); // [1, 3, 5] console.log(a1 instanceof SuperArray); // true console.log(a2 instanceof SuperArray); // false
类混入
把不同类的行为集中到一个类是一种常见的
JavaScript
模式。ES6
没有显式支持多类继承,但可以通过现有特征模拟。extends
后可以跟任何能够解析为一个类或一个构造函数的表达式。class Vehicle {} function getParentClass() { console.log('evaluated expression'); return Vehicle; } class Bus extends getParentClass() {} // 可求值的表达式
多类继承可以通过嵌套多个函数来实现:
class Vehicle {} let FooMixin = Superclass => class extends Superclass { foo() { console.log('foo'); } }; let BarMixin = Superclass => class extends Superclass { bar() { console.log('bar'); } }; let BazMixin = Superclass => class extends Superclass { baz() { console.log('baz'); } }; class Bus extends FooMixin(BarMixin(BazMixin(Vehicle))) {} let b = new Bus(); b.foo(); // foo b.bar(); // bar b.baz(); // baz
也可以通过辅助函数来展开嵌套调用:
class Vehicle {} let FooMixin = Superclass => class extends Superclass { foo() { console.log('foo'); } }; let BarMixin = Superclass => class extends Superclass { bar() { console.log('bar'); } }; let BazMixin = Superclass => class extends Superclass { baz() { console.log('baz'); } }; function mix(BaseClass, ...Mixins) { return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass); } class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {} let b = new Bus(); b.foo(); // foo b.bar(); // bar b.baz(); // baz
很多
JS
框架 (例如React
) 已经抛弃混入模式,转向了组合模式。把方法提取到独立的类和辅助对象中,然后把它们组合起来,但不使用继承。
现代 JavaScript 教程
任务
你好,对象
按下面的要求写代码,一条对应一行代码:
- 创建一个空的对象
user
。 - 为这个对象增加一个属性,键是
name
,值是John
。 - 再增加一个属性,键是
surname
,值是Smith
。 - 把键为
name
的属性的值改成Pete
。 - 删除这个对象中键为
name
的属性。
let user = {}; user.name = 'John'; user.surname = 'Smith'; user.name = 'Pete'; delete user.name;
- 创建一个空的对象
检查空对象
写一个
isEmpty(obj)
函数,当对象没有属性的时候返回true
,否则返回false
。应该像这样:
let schedule = {}; alert( isEmpty(schedule) ); // true schedule["8:30"] = "get up"; alert( isEmpty(schedule) ); // false
function isEmpty(obj) { for (let key in obj) { return false; } return true; }
对象属性求和
我们有一个保存着团队成员工资的对象:
let salaries = { John: 100, Ann: 160, Pete: 130 }
写一段代码求出我们的工资总和,将计算结果保存到变量
sum
。从所给的信息来看,结果应该是390
。如果
salaries
是一个空对象,那结果就为0
。let sum = 0; for (let key in salaries) { sum += salaries[key]; } console.log(sum);
数值属性都乘以2
创建一个
multiplyNumeric(obj)
函数,把obj
所有的数值属性都乘以2
。例如:
// 在调用之前 let menu = { width: 200, height: 300, title: "My menu" }; multiplyNumeric(menu); // 调用函数之后 menu = { width: 400, height: 600, title: "My menu" };
注意
multiplyNumeric
函数不需要返回任何值,它应该就地修改对象。P.S. 用
typeof
检查值类型。function multiplyNumeric(obj) { for (let key in obj) { let prev = obj[key]; if (typeof prev === 'number') { obj[key] = prev * 2; } } }
MDN 学习记录
重设所有属性值
可以设置一个属性叫 all
,来进行所有属性的修改,值可以是 inherit
initial
unset
revert
。
选择器
在 CSS
规则中,一个选择器无效,该选择器所对应的属性都将不生效。
值选择器
这些选择器允许基于一个元素自身是否存在(例如href
)或者基于各式不同的按属性值的匹配,来选取元素。
选择器 | 示例 | 描述 |
---|---|---|
[attr] |
a[title] |
匹配带有一个名为 attr 的属性的元素——方括号里的值。 |
[attr=value] |
a[href="https://example.com"] |
匹配带有一个名为 attr 的属性的元素,其值正为value——引号中的字符串。 |
[attr~=value] |
p[class~="special"] |
匹配带有一个名为 attr 的属性的元素 ,其值正为 value ,或者匹配带有一个 attr 属性的元素,其值有一个或者更多,至少有一个和value匹配。注意,在一列中的好几个值,是用空格隔开的。 |
`[attr | =value]` | `div[lang |
<h1>Attribute presence and value selectors</h1>
<ul>
<li>Item 1</li>
<li class="a">Item 2</li>
<li class="a b">Item 3</li>
<li class="ab">Item 4</li>
<li class="a-c">Item 5</li>
</ul>
li[class] {
font-size: 200%;
}
li[class="a"] {
background-color: yellow;
}
li[class~="a"] {
color: red;
}
li[class|="a"] {
color: blue;
}
子字符串匹配选择器
这些选择器让更高级的属性的值的子字符串的匹配变得可行。例如,如果你有box-warning
和box-error
类,想把开头为“box-”字符串的每个物件都匹配上的话,你可以用[class^="box-"]
来把它们两个都选中。
选择器 | 示例 | 描述 |
---|---|---|
[attr^=value] |
li[class^="box-"] |
匹配带有一个名为 attr 的属性的元素,其值开头为 value 子字符串。 |
[attr$=value] |
li[class$="-box"] |
匹配带有一个名为 attr 的属性的元素,其值结尾为 value 子字符串 |
[attr*=value] |
li[class*="box"] |
匹配带有一个名为 attr 的属性的元素,其值的字符串中的任何地方,至少出现了一次 value 子字符串。 |
大小写敏感
可以在中括号内,匹配条件后加入 i
表示忽略大小写。
li[class^="a"] {
background-color: yellow;
}
li[class^="a" i] {
color: red;
}
伪类
选择器 | 描述 |
---|---|
:active |
在用户激活(例如点击)元素的时候匹配。 |
:any-link |
匹配一个链接的:link 和:visited 状态。 |
:blank |
匹配空输入值的<input> 元素。 |
:checked |
匹配处于选中状态的单选或者复选框。 |
:current |
匹配正在展示的元素,或者其上级元素。 |
:default |
匹配一组相似的元素中默认的一个或者更多的UI元素。 |
:dir |
基于其方向性(HTMLdir 属性或者CSSdirection 属性的值)匹配一个元素。 |
:disabled |
匹配处于关闭状态的用户界面元素 |
:empty |
匹配除了可能存在的空格外,没有子元素的元素。 |
:enabled |
匹配处于开启状态的用户界面元素。 |
:first |
匹配分页媒体的第一页。 |
:first-child |
匹配兄弟元素中的第一个元素。 |
:first-of-type |
匹配兄弟元素中第一个某种类型的元素。 |
:focus |
当一个元素有焦点的时候匹配。 |
:focus-visible |
当元素有焦点,且焦点对用户可见的时候匹配。 |
:focus-within |
匹配有焦点的元素,以及子代元素有焦点的元素。 |
:future |
匹配当前元素之后的元素。 |
:hover |
当用户悬浮到一个元素之上的时候匹配。 |
:indeterminate |
匹配未定态值的UI元素,通常为复选框。 |
:in-range |
用一个区间匹配元素,当值处于区间之内时匹配。 |
:invalid |
匹配诸如<input> 的位于不可用状态的元素。 |
:lang |
基于语言(HTMLlang属性的值)匹配元素。 |
:last-child |
匹配兄弟元素中最末的那个元素。 |
:last-of-type |
匹配兄弟元素中最后一个某种类型的元素。 |
:left |
在分页媒体中,匹配左手边的页。 |
:link |
匹配未曾访问的链接。 |
:local-link |
匹配指向和当前文档同一网站页面的链接。 |
:is() |
匹配传入的选择器列表中的任何选择器。 |
:not |
匹配作为值传入自身的选择器未匹配的物件。 |
:nth-child |
匹配一列兄弟元素中的元素——兄弟元素按照an+b形式的式子进行匹配(比如2n+1匹配元素1、3、5、7等。即所有的奇数个)。 |
:nth-of-type |
匹配某种类型的一列兄弟元素(比如,<p> 元素)——兄弟元素按照an+b形式的式子进行匹配(比如2n+1匹配元素1、3、5、7等。即所有的奇数个)。 |
:nth-last-child |
匹配一列兄弟元素,从后往前倒数。兄弟元素按照an+b形式的式子进行匹配(比如2n+1匹配按照顺序来的最后一个元素,然后往前两个,再往前两个,诸如此类。从后往前数的所有奇数个)。 |
:nth-last-of-type |
匹配某种类型的一列兄弟元素(比如,<p> 元素),从后往前倒数。兄弟元素按照an+b形式的式子进行匹配(比如2n+1匹配按照顺序来的最后一个元素,然后往前两个,再往前两个,诸如此类。从后往前数的所有奇数个)。 |
:only-child |
匹配没有兄弟元素的元素。 |
:only-of-type |
匹配兄弟元素中某类型仅有的元素。 |
:optional |
匹配不是必填的form元素。 |
:out-of-range |
按区间匹配元素,当值不在区间内的的时候匹配。 |
:past |
匹配当前元素之前的元素。 |
:placeholder-shown |
匹配显示占位文字的input元素。 |
:playing |
匹配代表音频、视频或者相似的能“播放”或者“暂停”的资源的,且正在“播放”的元素。 |
:paused |
匹配代表音频、视频或者相似的能“播放”或者“暂停”的资源的,且正在“暂停”的元素。 |
:read-only |
匹配用户不可更改的元素。 |
:read-write |
匹配用户可更改的元素。 |
:required |
匹配必填的form元素。 |
:right |
在分页媒体中,匹配右手边的页。 |
:root |
匹配文档的根元素。 |
:scope |
匹配任何为参考点元素的的元素。 |
:valid |
匹配诸如<input> 元素的处于可用状态的元素。 |
:target |
匹配当前URL目标的元素(例如如果它有一个匹配当前URL分段的元素)。 |
:visited |
匹配已访问链接。 |
伪元素
选择器 | 描述 |
---|---|
::after |
匹配出现在原有元素的实际内容之后的一个可样式化元素。 |
::before |
匹配出现在原有元素的实际内容之前的一个可样式化元素。 |
::first-letter |
匹配元素的第一个字母。 |
::first-line |
匹配包含此伪元素的元素的第一行。 |
::grammar-error |
匹配文档中包含了浏览器标记的语法错误的那部分。 |
::selection |
匹配文档中被选择的那部分。 |
::spelling-error |
匹配文档中包含了浏览器标记的拼写错误的那部分。 |
书写模式、块级布局和内联布局
设置 writing-mode
属性可以更改元素的布局方向。
writing-mode
的三个值分别是:
horizontal-tb
: 块流向从上至下。对应的文本方向是横向的。vertical-rl
: 块流向从右向左。对应的文本方向是纵向的。vertical-lr
: 块流向从左向右。对应的文本方向是纵向的。
书写模式切换时,也在改变块布局和内联布局。
常见的网页是块布局在垂直方向,内联布局在水平方向。
纵向书写模式下块布局在水平方向,内联布局在垂直方向。
以上图片来源于 MDN
逻辑属性和逻辑值
在不同书写模式下, width
和 height
失去了原本的意义,会导致错误的宽高显示。于是诞生了新的概念属性,叫做 inline-size
和 block-size
。分别对应块布局的长度和内联布局的长度。
而外边距、内边距也有了对应的映射属性:
margin-top
属性的映射是 margin-block-start
——总是指向块级维度开始处的边距。
padding-left
属性映射到 padding-inline-start
,这是应用到内联开始方向(这是该书写模式文本开始的地方)上的内边距。
border-bottom
属性映射到的是 border-block-end
,也就是块级维度结尾处的边框。
属性中的 top
等值也有逻辑映射值:
top
、right
、bottom
和left
对应 block-start
、inline-end
、block-end
和inline-start
。
HSL 和 HSLA 的值
hsl()
接受色调、饱和度和亮度作为参数。
- 色调: 颜色的底色。这个值在0和360之间,表示色轮周围的角度。
- 饱和度: 颜色有多饱和? 它的值为0 - 100%,其中0为无颜色(它将显示为灰色阴影),100%为全色饱和度
- 亮度:颜色有多亮? 它从0 - 100%中获取一个值,其中0表示没有光(它将完全显示为黑色),100%表示完全亮(它将完全显示为白色)
hsla()
额外接受一个不透明度通道。