0%

学习笔记 2020 10 18

学习笔记 2020-10-18

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

对象、类与面向对象编程

增强的对象语法

ES6 新增了很多极其有用的语法糖特性。

  1. 属性值简写
    let name = 'Sirine';
    
    let person = {
      name
    };
    
    console.log(person); // {name: "Sirine"}

    简写属性名只要使用变量名就会自动解释为同名的属性键,如果没有找到同名变量,则会抛出 ReferenceError

  2. 可计算属性

    以前,想使用变量的值作为属性,必须先声明对象,使用中括号语法来添加属性。

    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' }
  3. 简写方法名

    旧的方法定义方式:

    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() 把源数据结构转换为对象。这意味着,原始值会被当成对象。 nullundefined 不能解构。否则会抛出 TypeError

如果是给事先声明的变量赋值,则赋值表达式需要包含在一对括号中。

let personName, personAge;
let person = {
  name: 'Sirine',
  age: 27
};
({ name: personName, age: personAge } = person);
console.log(personName, personAge); // Sirine, 27
  1. 嵌套解构
    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
  2. 部分解构

    涉及多个属性的解构赋值是一个输出无关的顺序化操作。如果开始赋值成功而后面赋值出错,解构赋值只能完成一部分。

    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
  3. 参数上下文匹配

    在函数参数列表中可以进行解构赋值,不会影响 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 操作符。会执行以下操作:

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

定义自定义构造函数可以确保实例被标识为特定类型。

构造函数也可以是被赋值给变量的函数表达式。

在实例化时,构造函数后的括号不加也可以。只要有 new 操作符,就可以调用相应的构造函数。

  1. 构造函数也是函数

    任何函数只要使用 new 操作符调用就是构造函数,不使用 new 操作符调用就是普通函数。

  2. 构造函数的问题

    构造函数的问题是定义的方法会在每个实例上创建一遍。每个实例都会有自己的方法,而这些方法实际上是一样的,却指向不同的引用,这是没有必要的。如果我们选择把方法抽离到全局,让每个实例的方法都指向这个全局方法,可以保证不会重复声明函数,但这样污染了全局作用域,当构造函数的方法较多时,全局上就会多出很多不必要的函数,这些函数明明只是属于我们的实例对象,却定义在了全局。

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;
}

image-20201018112427598

另外,测试了一下浮动元素应用 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;
}

image-20201018111957887

可以看出来,浮动元素清除浮动后会影响到后续的元素,后续的元素只会跟着它进行排列。不会高于它。

.bottomclear 修改为 left

image-20201018112058725

将底部的最后那个元素修改为右浮动。

image-20201018112132997