0%

学习笔记 2020 10 22

学习笔记 2020-10-22

JavaScript 高级程序设计(第4版) 阅读记录

对象、类与面向对象编程

实例、原型和类成员
  1. 实例成员

  2. 原型方法与访问器

    在类块中定义的方法作为原型方法。

    可以把方法定义在类构造函数中或者类块中,但不能在类块中给原型添加原始值或对象作为成员数据。

    类方法等同于对象属性,可以使用字符串、符号或计算的值作为键。

    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
  3. 静态类方法

    静态类成员在类定义中使用 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_: ... }
  4. 非函数原型和类成员

    在类的外部,可以给原型手动添加属性。

  5. 迭代器与生成器语法

    类定义语法支持在原型和类本身上定义生成器方法:

    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 的类继承使用了新语法,但背后依然使用原型链。

  1. 继承基础

    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 关键字也可以在类表达式中使用。

  2. 构造函数、 HomeObjectsuper()

    在派生类的构造函数中调用 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() ,要么必须在其中返回一个对象。
  3. 抽象基类

    抽象基类是指可被其他类继承但自身不会被实例化的类。 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()

    此处的原理是原型函数在调用类构造函数之前就已经存在,以此来进行判断。

  4. 继承内置类型

    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
  5. 类混入

    把不同类的行为集中到一个类是一种常见的 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 教程

任务

  1. 你好,对象

    按下面的要求写代码,一条对应一行代码:

    1. 创建一个空的对象 user
    2. 为这个对象增加一个属性,键是 name,值是 John
    3. 再增加一个属性,键是 surname,值是 Smith
    4. 把键为 name 的属性的值改成 Pete
    5. 删除这个对象中键为 name 的属性。
    let user = {};
    user.name = 'John';
    user.surname = 'Smith';
    user.name = 'Pete';
    delete user.name;
  2. 检查空对象

    写一个 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;
    }
  3. 对象属性求和

    我们有一个保存着团队成员工资的对象:

    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);
  4. 数值属性都乘以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;
}

image-20201022130858506

子字符串匹配选择器

这些选择器让更高级的属性的值的子字符串的匹配变得可行。例如,如果你有box-warningbox-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: 块流向从左向右。对应的文本方向是纵向的。

书写模式切换时,也在改变块布局和内联布局。

常见的网页是块布局在垂直方向,内联布局在水平方向。

纵向书写模式下块布局在水平方向,内联布局在垂直方向。

img

img

以上图片来源于 MDN

逻辑属性和逻辑值

在不同书写模式下, widthheight 失去了原本的意义,会导致错误的宽高显示。于是诞生了新的概念属性,叫做 inline-sizeblock-size 。分别对应块布局的长度和内联布局的长度。

而外边距、内边距也有了对应的映射属性:

margin-top 属性的映射是 margin-block-start ——总是指向块级维度开始处的边距。

padding-left 属性映射到 padding-inline-start ,这是应用到内联开始方向(这是该书写模式文本开始的地方)上的内边距。

border-bottom 属性映射到的是 border-block-end ,也就是块级维度结尾处的边框。

属性中的 top 等值也有逻辑映射值:

toprightbottomleft 对应 block-startinline-endblock-endinline-start

HSL 和 HSLA 的值

hsl() 接受色调、饱和度和亮度作为参数。

  • 色调: 颜色的底色。这个值在0和360之间,表示色轮周围的角度。
  • 饱和度: 颜色有多饱和? 它的值为0 - 100%,其中0为无颜色(它将显示为灰色阴影),100%为全色饱和度
  • 亮度:颜色有多亮? 它从0 - 100%中获取一个值,其中0表示没有光(它将完全显示为黑色),100%表示完全亮(它将完全显示为白色)

hsla() 额外接受一个不透明度通道。