用Object构造函数或对象字面量创建对象,有明显不足:创建具有同样接口的多个对象需要重复编写很多代码。
    8.2.1 概述综观
    ECMAScript 6开始正式支持类和继承。ES6的类旨在完全涵盖之前规范设计的基于原型的继承模式。不过,无论从哪方面看,ES6的类都仅仅是封装了ES5.1构造函数加原型继承的语法糖而已。
    8.2.2 工厂模式
    工厂模式是一种众所周知的设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。
    (本书后面还会讨论其他设计模式及其在JavaScript中的实现。)
    下面的例子展示了一种按照特定接口创建对象的方式:

    1. function createPerson( name, age, job ) {
    2. let o = new Object();
    3. o.name = name;
    4. o.age = age;
    5. o.job = job;
    6. o.sayName = function() {
    7. console.log(this.name);
    8. }
    9. return o;
    10. }
    11. let person1 = createPerson('Amy', 29, 'engineer');
    12. let person2 = createPerson('Bill', 27, 'Doctor');

    函数createPerson()接收3个参数,根据这几个参数构建了一个包含Person信息的对象。可以用不同的参数多次调用这个函数,每次都会返回包含3个属性和1个方法的对象。这种工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)。
    8.2.3 构造函数模式
    ECMAScript中的构造函数是用于创建特定类型对象的。
    像Object和Array这样的原生构造函数,运行时可以直接在执行环境中使用。
    当然也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。
    比如,前面的例子使用构造函数模式可以这样写:

    1. function Person( name, age, job ) {
    2. this.name = name;
    3. this.age = age;
    4. this.job = job;
    5. this.sayName = function() {
    6. console.log(this.name);
    7. }
    8. }
    9. let person1 = new Person('Amy', 29, 'engineer');
    10. let person2 = new Person('Bill', 27, 'Doctor');
    11. person1.sayName(); // Amy
    12. person2.sayName(); // Bill

    Person()构造函数代替了createPerson()工厂函数。
    实际上,Person()内部的代码跟createPerson()基本是一样的,只是有如下区别:
    ❑ 没有显式地创建对象。
    ❑ 属性和方法直接赋值给了this。
    ❑ 没有return。
    注:函数名Person的首字母大写了。
    按照惯例,构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头。
    这是从面向对象编程语言那里借鉴的,有助于在ECMAScript中区分构造函数和普通函数。
    要创建Person的实例,应使用new操作符。
    以这种方式调用构造函数会执行如下操作:
    (1)在内存中创建一个新对象。
    (2)这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype属性。
    (3)构造函数内部的this被赋值为这个新对象(即this指向新对象)。
    (4)执行构造函数内部的代码(给新对象添加属性)。
    (5)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
    上一个例子的最后,person1和person2分别保存着Person的不同实例。这两个对象都有一个constructor属性指向Person,如下所示:

    1. console.log(person1.constructor === Person); // true
    2. console.log(person2.constructor === Person); // true

    constructor本来是用于标识对象类型的。不过,一般认为instanceof操作符是确定对象类型更可靠的方式。前面例子中的每个对象都是Object的实例,同时也是Person的实例
    定义自定义构造函数可以确保实例被标识为特定类型,相比于工厂模式,这是一个很大的好处。
    在这个例子中,person1和person2之所以也被认为是Object的实例,是因为所有自定义对象都继承自Object
    构造函数不一定要写成函数声明的形式。
    赋值给变量的函数表达式也可以表示构造函数:

    1. let Person = function( name, age, job ) {
    2. this.name = name;
    3. this.age = age;
    4. this.job = job;
    5. this.sayName = function() {
    6. console.log(this.name);
    7. }
    8. }
    9. let person1 = new Person('Amy', 29, 'engineer');
    10. let person2 = new Person('Bill', 27, 'Doctor');

    在实例化时,如果不想传参数,那么构造函数后面的括号可加可不加。
    只要有new操作符,就可以调用相应的构造函数:

    1. let Person = function( name, age, job ) {
    2. this.name = name;
    3. this.age = age;
    4. this.job = job;
    5. this.sayName = function() {
    6. console.log(this.name);
    7. }
    8. }
    9. let person1 = new Person();
    10. let person2 = new Person;

    1.构造函数也是函数
    构造函数与普通函数唯一的区别就是调用方式不同。除此之外,构造函数也是函数。
    并没有把某个函数定义为构造函数的特殊语法。
    任何函数只要使用new操作符调用就是构造函数,而不使用new操作符调用的函数就是普通函数。
    2.构造函数的问题
    主要问题在于,其定义的方法会在每个实例上都创建一遍。
    因此对前面的例子而言,person1和person2都有名为sayName()的方法,但这两个方法不是同一个Function实例。当然了,以这种方式创建函数会带来不同的作用域链和标识符解析。但创建新Function实例的机制是一样的。因此不同实例上的函数虽然同名却不相等
    因为都是做一样的事,所以没必要定义两个不同的Function实例。
    况且,this对象可以把函数与对象的绑定推迟到运行时。
    要解决这个问题,可以把函数定义转移到构造函数外部:

    1. function Person( name, age, job ) {
    2. this.name = name;
    3. this.age = age;
    4. this.job = job;
    5. this.sayName = sayName; //注意这一行
    6. }
    7. function sayName() {
    8. console.log(this.name);
    9. }
    10. let person1 = new Person('Amy', 29, 'engineer');
    11. let person2 = new Person('Bill', 27, 'Doctor');
    12. person1.sayName(); // Amy
    13. person2.sayName(); // Bill

    sayName()被定义在了构造函数外部。
    在构造函数内部,sayName属性等于全局sayName()函数。
    因为这一次sayName属性中包含的只是一个指向外部函数的指针,所以person1和person2共享了定义在全局作用域上的sayName()函数。
    这样虽然解决了相同逻辑的函数重复定义的问题,但全局作用域也因此被搞乱了,因为那个函数实际上只能在一个对象上调用。如果这个对象需要多个方法,那么就要在全局作用域中定义多个函数。这会导致自定义类型引用的代码不能很好地聚集一起。这个新问题可以通过原型模式来解决。
    8.2.4 原型模式
    每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。
    使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型
    如下所示:

    1. function Person() {};
    2. Person.prototype.name = 'Amy';
    3. Person.prototype.age = 29;
    4. Person.prototype.sayName = function() {
    5. console.log(this.name);
    6. }
    7. let person1 = new Person();
    8. let person2 = new Person();
    9. person1.sayName(); // Amy
    10. person2.sayName(); // Amy
    11. console.log(person1.sayName === person2.sayName) // true

    使用函数表达式也可以:

    1. let Person = function(){};

    1.理解原型
    只要创建一个函数,就会为这个函数创建一个prototype属性(指向原型对象)。
    默认,所有原型对象自动获得一个名为constructor的属性,指回与之关联的构造函数。
    对前面的例子而言,Person.prototype.constructor指向Person。
    在自定义构造函数时,原型对象默认只会获得constructor属性,其他的所有方法都继承自Object。
    每次调用构造函数创建一个新实例,这个实例的内部[[Prototype]]指针就会被赋值为构造函数的原型对象。
    脚本中没有访问这个[[Prototype]]特性的标准方式,但Firefox、Safari和Chrome会在每个对象上暴露proto属性,通过这个属性可以访问对象的原型。
    关键在于理解这一点:实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。
    构造函数.protoType ==> 原型对象
    实例.proto ==> 原型对象
    实例.proto.constructor ==> 构造函数
    本质上,isPrototypeOf()会在传入参数的[[Prototype]]指向调用它的对象时返回true,
    如下所示:

    1. console.log(Person.prototype.isPrototypeOf(person1)); // true
    2. console.log(Person.prototype.isPrototypeOf(person2)); // true

    通过原型对象调用isPrototypeOf()方法检查了person1和person2。因为这两个例子内部都有链接指向Person.prototype,所以结果都返回true
    ECMAScript的Object类型有一个方法叫Object.getPrototypeOf(),返回参数的内部特性[[Prototype]]的值。
    使用Object.getPrototypeOf()可以方便地取得一个对象的原型,而这在通过原型实现继承时显得尤为重要
    例如:

    1. console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
    2. console.log(Object.getPrototypeOf(person1).name); // Amy

    第一行代码简单确认了Object.getPrototypeOf()返回的对象就是传入对象的原型对象。第二行代码则取得了原型对象上name属性的值,即”Amy”。
    Object类型还有一个setPrototypeOf()方法,可以向实例的私有特性[[Prototype]]写入一个新值。
    这样就可以重写一个对象的原型继承关系。
    注: 会严重影响代码性能
    为避免使用Object.setPrototypeOf()可能造成的性能下降,可以通过Object.create()来创建一个新对象,同时为其指定原型
    2.原型层级
    在通过对象访问属性时,会按照这个属性的名称开始搜索。
    搜索开始于对象实例本身。
    如果在这个实例上发现了给定的名称,则返回该名称对应的值。
    如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值。
    因此,在调用person1.sayName()时,会发生两步搜索。
    首先,JavaScript引擎会问:“person1实例有sayName属性吗?”
    答案是没有。
    然后,继续搜索并问:“person1的原型有sayName属性吗?”
    答案是有。
    于是就返回了保存在原型上的这个函数。
    这就是原型用于在多个对象实例间共享属性和方法的原理。
    虽然可以通过实例读取原型对象上的值,但不可能通过实例重写这些值。
    如果在实例上添加了一个与原型对象中同名的属性,那就会在实例上创建这个属性,这个属性会遮住原型对象上的属性。
    即使在实例上把这个属性设置为null,也不会恢复它和原型的联系。
    使用delete操作符可以完全删除实例上的这个属性,从而让标识符解析过程能够继续搜索原型对象。
    hasOwnProperty()方法用于确定某个属性是在实例上还是在原型对象上。
    这个方法是继承自Object的,会在属性存在于调用它的对象实例上时返回true

    1. function Person() {};
    2. Person.prototype.name = 'Amy';
    3. Person.prototype.age = 29;
    4. let person1 = new Person();
    5. person1.name = 'Bill'
    6. console.log(person1.hasOwnProperty('name')); // true
    7. console.log(person1.hasOwnProperty('age')); // false
    8. // 来自实例,则为true
    9. // 来自原型,则为false

    通过调用hasOwnProperty()能够清楚地看到访问的是实例属性还是原型属性
    3.原型和in操作符
    有两种方式使用in操作符:单独使用;在for-in循环中使用。
    在单独使用时,in操作符在可以通过对象访问指定属性时返回true,无论该属性是在实例上还是在原型上。
    来看下面的例子:

    1. function Person() {};
    2. Person.prototype.name = 'Amy';
    3. Person.prototype.age = 29;
    4. let person1 = new Person();
    5. person1.name = 'Bill'
    6. console.log('name' in person1); // true
    7. console.log('age' in person1); // true

    如果要确定某个属性是否存在于原型上,则可以像下面这样同时使用hasOwnProperty()和in操作符:

    1. function Person() {};
    2. Person.prototype.name = 'Amy';
    3. Person.prototype.age = 29;
    4. let person1 = new Person();
    5. person1.name = 'Bill'
    6. function hasPrototypeProperty(object, name) { // 就是这个
    7. return !object.hasOwnProperty(name) && (name in object);
    8. }
    9. console.log(hasPrototypeProperty(person1, 'name')); // false
    10. console.log(hasPrototypeProperty(person1, 'age')); // true
    11. // 首先存在于实例上,则返回false
    12. // 没在实例上,在原型上,则返回true
    13. // 即检验属性是否存在于实例上

    在for-in循环中使用in操作符时,可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性。
    遮蔽原型中不可枚举([[Enumerable]]特性被设置为false)属性的实例属性也会在for-in循环中返回,因为默认情况下开发者定义的属性都是可枚举的。
    要获得对象上所有可枚举的实例属性,可以使用Object.keys()方法。接收一个对象作为参数,返回包含该对象所有可枚举属性名称的字符串数组。
    比如:

    1. function Person() {};
    2. Person.prototype.name = 'Amy';
    3. Person.prototype.age = 29;
    4. let keys = Object.keys(Person.prototype);
    5. console.log(keys); // ["name", "age"]

    想列出所有实例属性,无论是否可以枚举,都可以使用Object.getOwnPropertyNames():

    1. function Person() {};
    2. Person.prototype.name = 'Amy';
    3. Person.prototype.age = 29;
    4. let keys = Object.getOwnPropertyNames(Person.prototype);
    5. console.log(keys); // ["constructor", "name", "age"]

    注:返回的结果中包含了一个不可枚举的属性constructor
    Object.keys()和Object.getOwnPropertyNames()在适当的时候都可用来代替for-in循环。
    Object.getOwnProperty-Symbols()方法,与Object.getOwnPropertyNames()类似,只是针对符号而已
    4.属性枚举顺序
    for-in循环、Object.keys()、Object.getOwnPropertyNames()、Object.getOwnProperty-Symbols()以及Object.assign()在属性枚举顺序方面有很大区别。
    for-in循环和Object.keys()的枚举顺序是不确定的。
    Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和Object.assign()的枚举顺序是确定性的。先以升序枚举数值键,然后以插入顺序枚举字符串和符号键。在对象字面量中定义的键以它们逗号分隔的顺序插入。
    image.png
    8.2.5 对象迭代
    两个静态方法Object.values()和Object.entries(),接收一个对象,返回它们内容的数组。
    Object.values()返回对象值的数组
    Object.entries()返回键/值对的数组
    用法:

    1. const o = {
    2. foo: 'bar',
    3. baz: 1,
    4. qux: {}
    5. };
    6. console.log(Object.values(o)); // [ 'baz', 1, {}]
    7. console.log(Object.entries(o));
    8. // [["foo", "bar"], ["baz", 1], ["qux", {}]]

    注意,非字符串属性会被转换为字符串输出
    符号属性会被忽略
    1.其他原型语法
    2.原型的动态性
    因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改也会在实例上反映出来
    3.原生对象原型
    原型模式之所以重要,不仅体现在自定义类型上,而且还因为它也是实现所有原生引用类型的模式。
    所有原生引用类型的构造函数(包括Object、Array、String等)都在原型上定义了实例方法
    比如,数组实例的sort()方法就是Array.prototype上定义的,而字符串包装对象的substring()方法也是在String.prototype上定义的
    4.原型的问题
    首先,它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值。
    虽然这会带来不便,但还不是原型的最大问题。
    原型的最主要问题源自它的共享特性。
    我们知道,原型上的所有属性是在实例间共享的,这对函数来说比较合适。
    另外包含原始值的属性也还好,如前面例子中所示,可以通过在实例上添加同名属性来简单地遮蔽原型上的属性。
    真正的问题来自包含引用值的属性。