0%

学习笔记 2020 10 21

学习笔记 2020-10-21

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

对象、类与面向对象编程

继承

原型式继承

原型式继承的方式为:

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}
let person = {
  name: 'Nicholas',
  friends: ['Shelby', 'Court', 'Van']
};
let anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');
let yetAnotherPerson = object(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');
console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"

继承时创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型,返回这个临时类型的一个实例。

这种继承方式适用于你在一个对象的基础上再创建一个新对象的情况。

这种方式已经有了规范化的方法,即 Object.create()

这个方法接收两个参数,第一个参数为作为新对象原型的对象,第二个可选参数为给新对象定义额外属性的对象,以这种方式添加的属性会遮蔽原型对象上的同名属性。

原型式继承跟使用原型模式是一样的。

寄生式继承

寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

function createAnother(original) {
  let clone = object(original); // 通过调用函数创建一个新对象
  clone.sayHi = function () {
    // 以某种方式增强这个对象
    console.log('hi');
  };
  return clone; // 返回这个对象
}
寄生式组合继承

组合继承的效率问题是父类构造函数始终会被调用两次:一次是在创建子类原型时调用,另一次是在子类构造函数中调用。

function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
  console.log(this.name);
};
function SubType(name, age) {
  SuperType.call(this, name); // 第二次调用 SuperType()
  this.age = age;
}
SubType.prototype = new SuperType(); // 第一次调用 SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function () {
  console.log(this.age);
};

寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。

即使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}
function inheritPrototype(subType, superType) {
  let prototype = object(superType.prototype); // 创建对象
  prototype.constructor = subType; // 增强对象
  subType.prototype = prototype; // 赋值对象
}
function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
  console.log(this.name);
};
function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function () {
  console.log(this.age);
};

只调用了一次 SuperType 构造函数,避免了 SubType.prototype 上不必要也用不到的属性。原型链保持不变, instanceof 操作符和 isPrototypeOf() 方法正常有效。

ES6 新引入了 class 关键字来正式定义类。

类定义

类可以使用声明或是表达式来定义。

// 类声明
class Person {}
// 类表达式
const Animal = class {};

并且类定义无法提升,且受块作用域限制。

类表达式有可选的名称,在类表达式作用域内部可以使用 name 属性取得名称字符串。

let Person = class PersonName {
  identify() {
    console.log(Person.name, PersonName.name);
  }
};
let p = new Person();
p.identify(); // PersonName PersonName
console.log(Person.name); // PersonName
console.log(PersonName); // ReferenceError: PersonName is not defined
类构造函数

constructor 关键字用于定义类的构造函数。构造函数的定义是可选的,不创建相当于定义为空函数。

使用 new 操作符实例化相当于调用了其构造函数。

使用 new 调用类的构造函数会执行如下操作:

  1. 在内存中创建一个新对象。
  2. 这个新对象内部的 [[Prototype]] 指针被赋值为构造函数的 prototype 属性。
  3. 构造函数内部的 this 被赋值为这个新对象。
  4. 执行构造函数内部的代码。
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的对象。

调用类构造函数如果忘了使用 new 会抛出错误。

ECMAScript 中没有正式的类这个类型。可以把 class 理解为一种特殊函数。

class Person {}
console.log(Person); // class Person {}
console.log(typeof Person); // function

类标识符有 prototype 属性,而原型有一个 constructor 属性指向类本身。

class Person {}
console.log(Person.prototype); // { constructor: f() }
console.log(Person === Person.prototype.constructor); // true

class Person {}
let p1 = new Person();
console.log(p1.constructor === Person); // true
console.log(p1 instanceof Person); // true
console.log(p1 instanceof Person.constructor); // false
let p2 = new Person.constructor();
console.log(p2.constructor === Person); // false
console.log(p2 instanceof Person); // false
console.log(p2 instanceof Person.constructor); // true

类可以像其他对象或函数引用一样作为参数传递。

// 类可以像函数一样在任何地方定义,比如在数组中
let classList = [
  class {
    constructor(id) {
      this.id_ = id;
      console.log(`instance ${this.id_}`);
    }
  }
];
function createInstance(classDefinition, id) {
  return new classDefinition(id);
}
let foo = createInstance(classList[0], 3141); // instance 3141

类也可以立即实例化。

// 因为是一个类表达式,所以类名是可选的
let p = new (class Foo {
  constructor(x) {
    console.log(x);
  }
})('bar'); // bar
console.log(p); // Foo {}
实例、原型和类成员

类的语法可以非常方便地定义应该存在于实例上的成员、应该存在于原型上的成员,以及应该存在于类本身的成员。

  1. 实例成员

    在构造函数内部可以定义实例成员。

MDN学习记录

表格

表格标签:

  • table
  • tr table row
  • td table description
  • th table header
  • caption 标题
  • thead 表格头部结构
  • tfoot 表格尾部结构
  • tbody 表格主体结构

最后三个标签不需要按顺序放置。

单元格跨越多行和列的方法是在需要变长或变高的单元格 td 标签上设置 colspanrowspan 属性。

为表格中的列提供相同样式

设置 colcolgroup 标签可以给表格的列设置相同的属性。会作用在对应的那一列上。

<table>
  <colgroup>
    <col />
    <col style="background-color: yellow" />
  </colgroup>
  <tr>
    <th>Data 1</th>
    <th>Data 2</th>
  </tr>
  <tr>
    <td>Calcutta</td>
    <td>Orange</td>
  </tr>
  <tr>
    <td>Robots</td>
    <td>Jazz</td>
  </tr>
</table>

表格演示

scope 属性

可以在 th 元素中添加 scope 属性来帮助屏幕阅读设备更好地理解那些标题单元格。

例如,可以表示当前的 th 标签到底是行标题还是列标题。

<thead>
  <tr>
    <th scope="col">Purchase</th>
    <th scope="col">Location</th>
    <th scope="col">Date</th>
    <th scope="col">Evaluation</th>
    <th scope="col">Cost (€)</th>
  </tr>
</thead>
<tr>
  <th scope="row">Haircut</th>
  <td>Hairdresser</td>
  <td>12/09</td>
  <td>Great idea</td>
  <td>30</td>
</tr>

scope 还有两个可选的值: colgroup rowgroup

还可以使用 idheaders 来创建标题与单元格之间的联系。

  1. 为每个 <th> 元素添加一个唯一的 id
  2. 为每个 <td> 元素添加一个 headers 属性。需要包含它从属于的所有标题的 id
<thead>
  <tr>
    <th id="purchase">Purchase</th>
    <th id="location">Location</th>
    <th id="date">Date</th>
    <th id="evaluation">Evaluation</th>
    <th id="cost">Cost (€)</th>
  </tr>
</thead>
<tbody>
<tr>
  <th id="haircut">Haircut</th>
  <td headers="location haircut">Hairdresser</td>
  <td headers="date haircut">12/09</td>
  <td headers="evaluation haircut">Great idea</td>
  <td headers="cost haircut">30</td>
</tr>

  ...

</tbody>