JavaScript高级程序设计 第4版 -- 对象、类与面向对象编程

本文深入探讨JavaScript中的对象、类和面向对象编程概念,包括理解对象、属性类型、对象的合并、对象标识及相等判定、增强的对象语法、创建对象(工厂模式、构造函数模式、原型模式)、继承(原型链、盗用构造函数、组合继承、原型式继承、寄生式继承、寄生组合式继承)以及ES6中的类定义、构造函数、继承等。
摘要由CSDN通过智能技术生成

第 8 章 对象、类与面向对象编程

8.1 理解对象

创建自定义对象的通常方式是创建 Object 的一个新实例,然后再给它添加属性和方法,如下例所示:

let person = new Object(); 
person.name = "Nicholas"; 
person.age = 29; 
person.job = "Software Engineer"; 
person.sayName = function() { 
 console.log(this.name); 
};

// 字面量写法
let person = { 
 name: "Nicholas", 
 age: 29, 
 job: "Software Engineer", 
 sayName() { 
 console.log(this.name); 
 } 
};

8.1.1 属性的类型

属性分两种:数据属性和访问器属性。

1、数据属性

包含一个保存数据值的位置,值会从这个位置读取,也会写入到这个位置,数据属性有4个特性描述它们的行为:

 [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true,如前面的例子所示。

 [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true,如前面的例子所示。

 [[Writable]]:表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的这个特性都是 true,如前面的例子所示。

 [[Value]]:包含属性实际的值。这就是前面提到的那个读取和写入属性值的位置。这个特性的默认值为 undefined。

let person = { 
 name: "Nicholas" 
};

这里,我们创建了一个名为 name 的属性,并给它赋予了一个值"Nicholas"。这意味着[[Value]]特性会被设置为"Nicholas",之后对这个值的任何修改都会保存这个位置。

要修改属性的默认特性,就必须使用 Object.defineProperty()方法。这个方法接收 3 个参数:要给其添加属性的对象、属性的名称和一个描述符对象

let person = {}; 
Object.defineProperty(person, "name", { 
 writable: false, 
 value: "Nicholas" 
}); 
console.log(person.name); // "Nicholas" 
person.name = "Greg"; 
console.log(person.name); // "Nicholas"

以上例子创建了一个名为name的属性并给它赋予了一个只读的值“Nicholas”,这个属性就不能再修改了,严格模式下,修改属性的值会抛出错误。

let person = {}; 
Object.defineProperty(person, "name", { 
  //不能从对象上删除
 configurable: false, 
 value: "Nicholas" 
}); 
console.log(person.name); // "Nicholas" 
delete person.name; 
console.log(person.name); // "Nicholas"

2、访问器属性

不包含数据值。他们包含一个获取(getter)函数和一个设置(setter)函数,在读取访问器属性时,会调用获取函数,返回一个有效的值,在写入访问器属性时,会调用设置函数并传入新值,有以下四个特性:

 [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。

 [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。

 [[Get]]:获取函数,在读取属性时调用。默认值为 undefined。

 [[Set]]:设置函数,在写入属性时调用。默认值为 undefined。

访问器属性是不能直接定义的,必须使用 Object.defineProperty()。

// 定义一个对象,包含伪私有成员 year_和公共成员 edition 
let book = { 
 year_: 2017, 
 edition: 1
 }; 
Object.defineProperty(book, "year", { 
 get() { 
 return this.year_; 
 }, 
 set(newValue) { 
 if (newValue > 2017) { 
 this.year_ = newValue; 
 this.edition += newValue - 2017; 
 } 
 } 
}); 
book.year = 2018; 
console.log(book.edition); // 2

8.1.2 定义多个属性

Object.defineProperties()

 let book = {};
    Object.defineProperties(book, {
      year_: {
        value: 2017,
      },
      edition: {
        value: 1,
      },
      year: {
        get() {
          return this.year_;
        },
        set(newValue) {
          if (newValue > 2017) {
            this.year_ = newValue;
            this.edition += newValue - 2017;
          }
        },
      },
    });

8.1.3 读取属性的特性

使用 Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符。

  let book = {};
  Object.defineProperties(book, {
    year_: {
      value: 2017
    },
    edition: {
      value: 1
    },
    year: {
      get: function () {
        return this.year_;
      },
      set: function (newValue) {
        if (newValue > 2017) {
          this.year_ = newValue;
          this.edition += newValue - 2017;
        }
      }
    }
  });
  let descriptor = Object.getOwnPropertyDescriptor(book, "year_"); 
  console.log(descriptor.value); // 2017 
  console.log(descriptor.configurable); // false 
  console.log(typeof descriptor.get); // "undefined" 
  let yearDescriptor = Object.getOwnPropertyDescriptor(book, "year"); 
  console.log(yearDescriptor.value); // undefined 
  console.log(yearDescriptor.enumerable); // false 
  console.log(typeof yearDescriptor.get); // "function"

ECMAScript 2017 新增了 Object.getOwnPropertyDescriptors()静态方法,实际上是在每个自有属性上调用Object.getOwnPropertyDescriptor()并在一个新对象中返回它们。

 let book = {};
    Object.defineProperties(book, {
      year_: {
        value: 2017,
      },
      edition: {
        value: 1,
      },
      year: {
        get: function () {
          return this.year_;
        },
        set: function (newValue) {
          if (newValue > 2017) {
            this.year_ = newValue;
            this.edition += newValue - 2017;
          }
        },
      },
    });
    console.log(Object.getOwnPropertyDescriptors(book));

在这里插入图片描述

8.1.4 合并对象

Object.assign()方法

let dest, src, result;
  /** 
   * 简单复制
   */
  dest = {};
  src = {
    id: 'src'
  };
  result = Object.assign(dest, src);
  // Object.assign 修改目标对象
  // 也会返回修改后的目标对象
  console.log(dest === result); // true 
  console.log(dest !== src); // true 
  console.log(result); // { id: src } 
  console.log(dest); // { id: src } 
  /** 
   * 多个源对象
   */
  dest = {};
  result = Object.assign(dest, {
    a: 'foo'
  }, {
    b: 'bar'
  });
  console.log(result); // { a: foo, b: bar } 
  /** 
   * 获取函数与设置函数
   */
  dest = {
    set a(val) {
      console.log(`Invoked dest setter with param ${val}`);
    }
  };
  src = {
    get a() {
      console.log('Invoked src getter');
      return 'foo';
    }
  };
  Object.assign(dest, src);
  // 调用 src 的获取方法
  // 调用 dest 的设置方法并传入参数"foo" 
  // 因为这里的设置函数不执行赋值操作
  // 所以实际上并没有把值转移过来
  console.log("=============>", dest); // { set a(val) {...} }

Object.assign()实际上对每个源对象执行的是浅复制。如果多个源对象都有相同的属性,则使用最后一个复制的值。此外,从源对象访问器属性取得的值,比如获取函数,会作为一个静态值赋给目标对象。换句话说,不能在两个对象间转移获取函数和设置函数。

let dest, src, result; 
/** 
 * 覆盖属性
 */ 
dest = { id: 'dest' }; 
result = Object.assign(dest, { id: 'src1', a: 'foo' }, { id: 'src2', b: 'bar' }); 
// Object.assign 会覆盖重复的属性
console.log(result); // { id: src2, a: foo, b: bar } 
// 可以通过目标对象上的设置函数观察到覆盖的过程:
dest = { 
 set id(x) { 
 console.log(x); 
 } 
}; 
Object.assign(dest, { id: 'first' }, { id: 'second' }, { id: 'third' }); 
// first 
// second 
// third 
/** 
 * 对象引用
 */ 
dest = {}; 
src = { a: {} }; 
Object.assign(dest, src); 
// 浅复制意味着只会复制对象的引用
console.log(dest); // { a :{} } 
console.log(dest.a === src.a); // true

8.1.5 对象标识及相等判定

在es6之前有些特殊情况是===操作符无法正确判断的

如:

// 这些情况在不同 JavaScript 引擎中表现不同,但仍被认为相等
console.log(+0 === -0); // true 
console.log(+0 === 0); // true 
console.log(-0 === 0); // true
// 要确定 NaN 的相等性,必须使用极为讨厌的 isNaN() 
console.log(NaN === NaN); // false 
console.log(isNaN(NaN)); // true

为此es6新增了Object.is()

// 正确的 0、-0、+0 相等/不等判定
console.log(Object.is(+0, -0)); // false 
console.log(Object.is(+0, 0)); // true 
console.log(Object.is(-0, 0)); // false 
// 正确的 NaN 相等判定
console.log(Object.is(NaN, NaN)); // true

8.1.6 增强的对象语法

1、属性值简写

let name = 'Matt'; 
let person = { 
 name: name 
}; 
console.log(person); // { name: 'Matt' }
// 等同于:
let name = 'Matt'; 
let person = { 
 name 
}; 
console.log(person); // { name: 'Matt' }

2、可计算属性

可以在对象字面量中完成动态属性复制

const nameKey = 'name'; 
const ageKey = 'age'; 
const jobKey = 'job'; 
let person = { 
 [nameKey]: 'Matt', 
 [ageKey]: 27, 
 [jobKey]: 'Software engineer' 
}; 
console.log(person); // { name: 'Matt', age: 27, job: 'Software engineer' }

3、简写方法名

let person = { 
 sayName: function(name) { 
 console.log(`My name is ${name}`); 
 } 
}; 
person.sayName('Matt'); // My name is Matt
//等同于
let person = { 
 sayName(name) { 
 console.log(`My name is ${name}`); 
 } 
}; 
person.sayName('Matt'); // My name is Matt

//简写方法名对获取函数和设置函数也是适用的:
 name_: '', 
 get name() { 
 return this.name_; 
 }, 
 set name(name) { 
 this.name_ = name; 
 }, 
 sayName() { 
 console.log(`My name is ${this.name_}`); 
 } 
}; 
person.name = 'Matt'; 
person.sayName(); // My name is Matt

//简写方法名与可计算属性键相互兼容:
const methodKey = 'sayName'; 
let person = { 
 [methodKey](name) { 
 console.log(`My name is ${name}`); 
 } 
} 

8.1.7 对象解构

// 使用对象解构
let person = { 
 name: 'Matt', 
 age: 27 
}; 
//另命名
let { name: personName, age: personAge } = person;
console.log(personName); // Matt 
console.log(personAge); // 27
//如果给事先声明的变量赋值,必须包含在一对括号中
let personName, personAge; 
let person = { 
 name: 'Matt', 
 age: 27 
}; 
({name: personName, age: personAge} = person); 
console.log(personName, personAge); // Matt, 27

//直接解构
let { name, age } = person; 
console.log(name); // Matt 
console.log(age); // 27
//如果引用的属性 不存在,报undefined
console.log(job); // undefined
//也可以在解构赋值的同时定义默认值
let { name, job='Software engineer' } = person;
console.log(job); // Software engineer

解构在内部使用函数 ToObject()(不能在运行时环境中直接访问)把源数据结构转换为对象。这意味着在对象解构的上下文中,原始值会被当成对象。这也意味着(根据 ToObject()的定义),null和 undefined 不能被解构,否则会抛出错误。

let { length } = 'foobar'; 
console.log(length); // 6 
let { constructor: c } = 4; 
console.log(c === Number); // true 
let { _ } = null; // TypeError 
let { _ } = undefined; // TypeError

1、嵌套解构

 let person = {
    name: 'Matt',
    age: 27,
    job: {
      title: 'Software engineer'
    }
  };
  let personCopy = {};
  ({
    name: personCopy.name,
    age: personCopy.age,
    job: personCopy.job
  } = person);
  // 因为一个对象的引用被赋值给 personCopy,所以修改 person.job 对象的属性也会影响 personCopy 
  person.job.title = 'Hacker'
  console.log(person);
   // { name: 'Matt', age: 27, job: { title: 'Hacker' } } 
  console.log(personCopy);
  // { name: 'Matt', age: 27, job: { title: 'Hacker' } }
//嵌套结构
let person = { 
 name: 'Matt', 
 age: 27, 
 job: { 
 title: 'Software engineer' 
 } 
}; 
// 声明 title 变量并将 person.job.title 的值赋给它
let { job: { title } } = person; 
console.log(title); // Software engineer

在外层属性没有定义的情况下不能使用嵌套解构

let person = { 
 job: { 
 title: 'Software engineer' 
 } 
}; 
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: 'Matt', 
 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); 
// Matt, undefined, undefined
  1. 参数上下文匹配

对参数的解构赋值不会影响 arguments 对象

8.2 创建对象

8.2.1、工厂模式

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("Nicholas", 29, "Software Engineer"); 
let person2 = createPerson("Greg", 27, "Doctor");

8.2.2、 构造函数模式

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("Nicholas", 29, "Software Engineer"); 
let person2 = new Person("Greg", 27, "Doctor"); 
person1.sayName(); // Nicholas 
person2.sayName(); // Greg

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

和工厂模式的区别

 没有显式地创建对象。

 属性和方法直接赋值给了 this。

 没有 return。

创建实例,使用了new操作符:

(1) 在内存中创建一个新对象。

(2) 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。

(3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。

(4) 执行构造函数内部的代码(给新对象添加属性)。

(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

1、构造函数也是函数

构造函数与普通函数唯一的区别就是调用方式不同。。任何函数只要使用 new 操作符调用就是构造函数,而不使用 new 操作符调用的函数就是普通函数。

 function Person(name, age, job) {
      this.name = name;
      this.age = age;
      this.job = job;
      this.sayName = function () {
        console.log(this.name);
      };
    }
	// 作为构造函数
    let person = new Person("Nicholas", 29, "Software Engineer");
    person.sayName(); // "Nicholas"
	// 作为函数调用
    Person("Greg", 27, "Doctor"); // 添加到 window 对象
    window.sayName(); // "Greg"
	// 在另一个对象的作用域中调用
    let o = new Object();
    Person.call(o, "Kristen", 25, "Nurse");
    o.sayName(); // "Kristen"

2、构造函数的问题

每次定义函数时,都会初始化一个对象

此对前面的例子而言,person1 和 person2 都有名为 sayName()的方法,但这两个方法不是同一个 Function 实例。

要解决这个问题,可以把函数定义转移到构造函数外部

function Person(name, age, job){ 
 this.name = name; 
 this.age = age; 
 this.job = job; 
 this.sayName = sayName; 
} 
function sayName() { 
 console.log(this.name); 
} 
let person1 = new Person("Nicholas", 29, "Software Engineer"); 
let person2 = new Person("Greg", 27, "Doctor"); 
person1.sayName(); // Nicholas 
person2.sayName(); // Greg

这样虽然解决了相同逻辑的函数重复定义的问题,但全局作用域也因此被搞乱了,因为那个函数实际上只能在一个对象上调用。如果这个对象需要多个方法,那么就要在全局作用域中定义多个函数。这会导致自定义类型引用的代码不能很好地聚集一起。这个新问题可以通过原型模式来解决。

8.2.4 原型模式

每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型

好处:在原型对象上定义的属性和方法可以被对象实例共享

​ 原来在构造函数中直接赋给对象实例的值,可以直接赋值给他们的原型

    function Person() {}
   // let Person = function() {};  //函数表达式也可以
    Person.prototype.name = "js";
    Person.prototype.sayName = function () {
      console.log(this.name);
    };
    let person1 = new Person();
    person1.sayName(); // "js"
    let person2 = new Person();
    person2.sayName(); // "js"
    console.log(person1.sayName == person2.sayName); // true

1、理解原型

只要创建一个函数,就会给函数创建一个prototype属性(指向原型对象),所有的原型对象自动获得一个名为constructor 的属性,指回与之关联的构造函数。

Person.prototype.constructor == Person //true

用代码理解实例与构造函数的关系:

   function Person() {}
      // 声明之后,构造函数就有了一个与之关联的原型对象:
    console.log(typeof Person.prototype);
    console.log(Person.prototype);
    // {
    // constructor: f Person(),
    // [[Prototype]]: Object
    // }

    // 如前所述,构造函数有一个 prototype 属性 引用其原型对象,而这个原型对象也有一个 constructor 属性,引用这个构造函数换句话说,两者循环引用:
    console.log(Person.prototype.constructor === Person); // true
    // 正常的原型链都会终止于 Object 的原型对象 Object 原型的原型是 null
    console.log(Person.prototype.__proto__ === Object.prototype); // true
    console.log(Person.prototype.__proto__.constructor === Object); // true
    console.log(Person.prototype.__proto__.__proto__ === null); // true
    console.log(Person.prototype.__proto__);
    // {
    // constructor: f Object(),
    // toString: ...
    // hasOwnProperty: ...
    // isPrototypeOf: ...
    // ...
    // }
    let person1 = new Person(),
      person2 = new Person();
    //构造函数、原型对象和实例 是 3 个完全不同的对象:
    console.log(person1 !== Person); // true
    console.log(person1 !== Person.prototype); // true
    console.log(Person.prototype !== Person); // true
    /**
     * 实例通过__proto__链接到原型对象,
     * 它实际上指向隐藏特性[[Prototype]]
     * 构造函数通过 prototype 属性链接到原型对象
     * 实例与构造函数没有直接联系,与原型对象有直接联系
     */
    console.log(person1.__proto__ === Person.prototype); // true
    console.log(person1.__proto__.constructor === Person); // true
    // 同一个构造函数创建的两个实例 共享同一个原型对象:
    console.log(person1.__proto__ === person2.__proto__); // true
    //instanceof 检查实例的原型链中 是否包含指定构造函数的原型:
    console.log(person1 instanceof Person); // true
    console.log(person1 instanceof Object); // true
    console.log(Person.prototype instanceof Object); // true

下图展示了各个对象之间的关系:

Person.prototype 指向原型对象,而 Person.prototype.contructor 指回 Person 构造函数。原型对象包含 constructor 属性和其他后来添加的属性。Person 的两个实例 person1 和 person2 都只有一个内部属性指回 Person.prototype,而且两者都与构造函数没有直接联系。另

在这里插入图片描述

判断两个对象之间的关系:

//两个例子内部都有链接指向Person.prototype
console.log(Person.prototype.isPrototypeOf(person1)); // true 
console.log(Person.prototype.isPrototypeOf(person2)); // true

ECMAScript 的 Object 类型有一个方法叫 Object.getPrototypeOf(),返回参数的内部特性

[[Prototype]]的值。

console.log(Object.getPrototypeOf(person1) == Person.prototype); // true 
console.log(Object.getPrototypeOf(person1).name); // "Nicholas"

Object 类型还有一个 setPrototypeOf()方法,可以向实例的私有特性[[Prototype]]写入一

个新值。这样就可以重写一个对象的原型继承关系:

 let biped = {
      numLegs: 2,
};
let person = {
  name: "Matt",
};
Object.setPrototypeOf(person, biped);
console.log(person.name); // Matt
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true

Object.setPrototypeOf会严重影响代码性能,可以通过Object.create()来创建一个新对象,并且为其指定原型:

    let biped = {
      numLegs: 2,
    };
    let person = Object.create(biped);
    person.name = "Matt";
    console.log(person.name); // Matt
    console.log(person.numLegs); // 2
    console.log(Object.getPrototypeOf(person) === biped); // true

2、 原型层级

在通过对象访问属性时,会按照这个属性的名称开始搜索,现在对象实例本身搜索,如果找到了就返回,找不到就在原型对象上搜索。

索然可以通过实例读取原型对象上的值,但不可能通过实例重写这些值,如果在实例上添加了一个与原型对象中同名的属性,那么会在实例上创建这个属性,并且会盖住原型对象上的属性,不会修改它,但是会屏蔽对他的访问。

使用delete可以完全删除实例上的这个属性,从而让标识符解析过程能够继续搜索原型对象

function Person() {}
    Person.prototype.name = "Nicholas";
    Person.prototype.age = 29;
    Person.prototype.job = "Software Engineer";
    Person.prototype.sayName = function () {
      console.log(this.name);
    };
    let person1 = new Person();
    let person2 = new Person();
    person1.name = "Greg";
    console.log(person1.name); // "Greg",来自实例
    console.log(person2.name); // "Nicholas",来自原型

hasOwnProperty()方法用于确定某个属性是在实例上还是在原型对象上。

 function Person() {}
  Person.prototype.name = "Nicholas";
  Person.prototype.age = 29;
  Person.prototype.job = "Software Engineer";
  Person.prototype.sayName = function () {
    console.log(this.name);
  };
  let person1 = new Person();
  let person2 = new Person();
  console.log(person1.hasOwnProperty("name")); // false
  person1.name = "Greg";
  console.log(person1.name); // "Greg",来自实例
  console.log(person1.hasOwnProperty("name")); // true
  console.log(person2.name); // "Nicholas",来自原型
  console.log(person2.hasOwnProperty("name")); // false
  delete person1.name;
  console.log(person1.name); // "Nicholas",来自原型
  console.log(person1.hasOwnProperty("name")); // false

3、原型和in操作符

单独使用时,in操作符会在可以通过对象访问指定属性时返回true,无论在实例上还是原型上

 function Person() {}
    Person.prototype.name = "Nicholas";
    let person1 = new Person();
    let person2 = new Person();
    console.log(person1.name); // "Greg",来自原型
    console.log(person1.hasOwnProperty("name")); // false
    console.log("name" in person1); // true
    person1.name = "Greg";
    console.log(person1.name); // "Greg",来自实例
    console.log(person1.hasOwnProperty("name")); // true
    console.log("name" in person1); // true

    delete person1.name;
    console.log(person1.name); // "Nicholas",来自原型
    console.log(person1.hasOwnProperty("name")); // false
    console.log("name" in person1); // true

如果要确定某个属性是否存在于原型上,则可以同时使用hasOwnProperty()和in

function hasPrototypeProperty(object, name){ 
 return !object.hasOwnProperty(name) && (name in object); 
}

获得对象所有可枚举的实例属性:Object.keys()

 function Person() {}
  Person.prototype.name = "Nicholas";
  Person.prototype.age = 29;
  Person.prototype.job = "Software Engineer";
  Person.prototype.sayName = function () {
  };
  console.log(Object.keys(Person.prototype)); // "name,age,job,sayName"
  let p1 = new Person();
  p1.name = "Rob";
  p1.age = 31;
  console.log(Object.keys(p1)); // "[name,age]"
  console.log(Object.keys(p1.__proto__)); // " ['name', 'age', 'job', 'sayName']"

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

let keys = Object.getOwnPropertyNames(Person.prototype); 
console.log(keys); // "[constructor,name,age,job,sayName]"

4、属性枚举顺序

1、不确定:for-in 循环和 Object.keys()

2、确定:Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和 Object.assign()

8.2.5 对象迭代

Object.values()返回对象值的数组;

Object.entries()返回键/值对的数组

注意:非字符串属性会被转换成字符串输出,这2个方法执行对象的浅复制

const sym = Symbol();
const o = { 
 foo: 'bar', 
 baz: 1, 
 qux: {},
 [sym]: 'foo' //符号属性会被忽略
}; 
console.log(Object.values(o)); // ["bar", 1, {}]
console.log(Object.entries((o))); // [["foo", "bar"], ["baz", 1], ["qux", {}]]

1、其他原型语法

字面量写法

function Person() {} 
Person.prototype = {
 name: "Nicholas", 
 age: 29, 
 job: "Software Engineer", 
 sayName() { 
 console.log(this.name); 
 } 
};

这样重写之后,Person.prototype 的 constructor 属性就不指向 Person了。在创建函数时,也会创建它的 prototype 对象,同时会自动给这个原型的 constructor 属性赋值。而上面的写法完全重写了默认的 prototype 对象,因此其 constructor 属性也指向了完全不同的新对象(Object 构造函数),不再指向原来的构造函数。虽然 instanceof 操作符还能可靠地返回值,但我们不能再依靠 constructor 属性来识别类型了。

let friend = new Person(); 
console.log(friend instanceof Object); // true 
console.log(friend instanceof Person); // true
console.log(friend.constructor == Person); // false 
console.log(friend.constructor == Object); // true

如果 constructor 的值很重要,则可以像下面这样在重写原型对象时专门设置一下它的值:

function Person() { 
} 
Person.prototype = { 
 constructor: Person, 
 name: "Nicholas", 
 age: 29, 
 job: "Software Engineer", 
 sayName() { 
 console.log(this.name); 
 } 
};

但这种方式会创建一个个[[Enumerable]]为 true 的属性。而原生 constructor 属性默认是不可枚举的。因此,如果你使用的是兼容 ECMAScript 的 JavaScript 引擎,那可能会改为使用 Object.defineProperty()方法来定义 constructor 属性:

function Person() {} 
Person.prototype = { 
 name: "Nicholas", 
 age: 29, 
 job: "Software Engineer", 
 sayName() { 
 console.log(this.name); 
 } 
}; 
// 恢复 constructor 属性
Object.defineProperty(Person.prototype, "constructor", { 
 enumerable: false, 
 value: Person 
});

8.3 继承

8.3.1 原型链

function SuperType() {
      this.property = true;
    }
    SuperType.prototype.getSuperValue = function () {
      return this.property;
    };

    function SubType() {
      this.subproperty = false;
    }
    // 继承 SuperType
    SubType.prototype = new SuperType();
    let instance = new SubType();
    console.log(instance.getSuperValue()); // true

2、原型与继承关系

console.log(instance instanceof Object); // true 
console.log(instance instanceof SuperType); // true 
console.log(instance instanceof SubType); // true

console.log(Object.prototype.isPrototypeOf(instance)); // true 
console.log(SuperType.prototype.isPrototypeOf(instance)); // true 
console.log(SubType.prototype.isPrototypeOf(instance)); // true

3、关于方法

子类会覆盖父类的方法:

 this.property = true; 
} 
SuperType.prototype.getSuperValue = function() { 
 return this.property; 
}; 
function SubType() { 
 this.subproperty = false; 
} 
// 继承 SuperType 
SubType.prototype = new SuperType(); 
// 新方法
SubType.prototype.getSubValue = function () { 
 return this.subproperty; 
}; 
// 覆盖已有的方法
SubType.prototype.getSuperValue = function () { 
 return false; 
}; 
let instance = new SubType(); 
console.log(instance.getSuperValue()); // false

以对象字面量方式创建原型方法会破坏之前的原型链,因为这相当于重写了原型链。

 this.property = true; 
} 
SuperType.prototype.getSuperValue = function() { 
 return this.property; 
}; 
function SubType() { 
 this.subproperty = false; 
}
// 继承 SuperType 
SubType.prototype = new SuperType(); 
// 通过对象字面量添加新方法,这会导致上一行无效
SubType.prototype = { 
 getSubValue() { 
 return this.subproperty; 
 }, 
 someOtherMethod() { 
 return false; 
 } 
}; 
let instance = new SubType(); 
console.log(instance.getSuperValue()); // 出错!

4、原型链的问题

1、原型中包含的引用值会在所有实例间共享

2、子类型在实例化时不能给父类型的构造函数传参

8.3.2 盗用构造函数

为了解决原型包含引用值导致的继承问题而出现。

思路:在子类构造函数中调用父类构造函数,通过apply()和 call()方法

function SuperType() { 
 this.colors = ["red", "blue", "green"]; 
} 
function SubType() { 
 // 继承 SuperType 
 SuperType.call(this); 
} 
let instance1 = new SubType(); 
instance1.colors.push("black"); 
console.log(instance1.colors); // "red,blue,green,black" 
let instance2 = new SubType(); 
console.log(instance2.colors); // "red,blue,green"

1、传递参数

function SuperType(name){ 
 this.name = name; 
} 
function SubType() { 
 // 继承 SuperType 并传参
 SuperType.call(this, "Nicholas"); 
 // 实例属性
 this.age = 29; 
} 
let instance = new SubType(); 
console.log(instance.name); // "Nicholas"; 
console.log(instance.age); // 29

2、盗用构造函数的问题

使用构造函数模式自定义类型的问题:必须在构造函数中定义方法,因此函数不能重用。

8.3.3 组合继承

综合了原型链和盗用构造函数,将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性

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; 
} 
// 继承方法
SubType.prototype = new SuperType(); 
SubType.prototype.sayAge = function() { 
 console.log(this.age); 
}; 
let instance1 = new SubType("Nicholas", 29); 
instance1.colors.push("black"); 
console.log(instance1.colors); // "red,blue,green,black" 
instance1.sayName(); // "Nicholas"; 
instance1.sayAge(); // 29 
let instance2 = new SubType("Greg", 27); 
console.log(instance2.colors); // "red,blue,green" 
instance2.sayName(); // "Greg"; 
instance2.sayAge(); // 27

8.3.4 原型式继承

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

function object(o) { 
 function F() {} 
 F.prototype = o; 
 return new F(); 
}

8.3.5 寄生式继承

创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

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

8.3.6 寄生式组合继承

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

8.4 类

8.4.1 类定义

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

1、与函数定义不同的是,虽然函数声明可以提升,但类定义不能

2、函数受函数作用域限制,而类受块作用域限制

类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法.

// 空类定义,有效 
class Foo {} 
// 有构造函数的类,有效
class Bar { 
 constructor() {} 
} 
// 有获取函数的类,有效
class Baz { 
 get myBaz() {} 
} 
// 有静态方法的类,有效
class Qux { 
 static myQux() {} 
}

8.4.2 类构造函数

方法名 constructor 会告诉解释器在使用 new 操作符创建类的新实例时,应该调用这个函数

1、实例化

(1)在内存中创建一个空对象

(2)这个新对象内部的[[Prototype]]指针被赋值为构造函数的prototype属性

(3)构造函数内部的this被赋值为这个新对象

(4)执行构造函数内部的代码(给新对象添加属性)

(5)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

类实例化时传入的参数会用作构造函数的参数。如果不需要参数,则类名后面的括号也是可选的:

class Person { 
 constructor(name) { 
 console.log(arguments.length); 
 this.name = name || null; 
 } 
} 
let p1 = new Person; // 0 
console.log(p1.name); // null 
let p2 = new Person(); // 0 
console.log(p2.name); // null 
let p3 = new Person('Jake'); // 1 
console.log(p3.name); // Jake

8.4.4 继承

1、继承基础:extends

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

2、构造函数、HomeObject 和 super()

派生类的方法可以通过 super 关键字引用它们的原型。这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。在类构造函数中使用 super 可以调用父类构造函数。

class Vehicle { 
 constructor() { 
 this.hasEngine = true; 
 } 
} 
class Bus extends Vehicle { 
 constructor() { 
 // 不要在调用 super()之前引用 this,否则会抛出 ReferenceError 
 super(); // 相当于 super.constructor() 
 console.log(this instanceof Vehicle); // true 
 console.log(this); // Bus { hasEngine: true } 
 } 
} 
new Bus(); 
在静态方法中可以通过 super 调用继承的类上定义的静态方法:
class Vehicle { 
 static identify() { 
 console.log('vehicle'); 
 } 
} 
class Bus extends Vehicle { 
 static identify() { 
 super.identify(); 
 } 
} 
Bus.identify(); // vehicle

在使用 super 时要注意几个问题。

 super 只能在派生类构造函数和静态方法中使用。

class Vehicle { 
 constructor() { 
 super(); 
 // SyntaxError: 'super' keyword unexpected 
 } 
} 

 不能单独引用 super 关键字,要么用它调用构造函数,要么用它引用静态方法。

class Vehicle {} 
class Bus extends Vehicle { 
 constructor() { 
 console.log(super); 
 // SyntaxError: 'super' keyword unexpected here 
 } 
} 

 调用 super()会调用父类构造函数,并将返回的实例赋值给 this。

class Vehicle {} 
class Bus extends Vehicle { 
 constructor() { 
 super(); 
 console.log(this instanceof Vehicle); 
 } 
} 
new Bus(); // true 

 super()的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入。

class Vehicle { 
 constructor(licensePlate) { 
 this.licensePlate = licensePlate; 
 } 
} 
class Bus extends Vehicle { 
 constructor(licensePlate) { 
 super(licensePlate); 
 } 
} 
console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' } 

 如果没有定义类构造函数,在实例化派生类时会调用 super(),而且会传入所有传给派生类的

参数。

class Vehicle { 
 constructor(licensePlate) { 
 this.licensePlate = licensePlate; 
 } 
} 
class Bus extends Vehicle {} 
console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' } 

 在类构造函数中,不能在调用 super()之前引用 this。

class Vehicle {} 
class Bus extends Vehicle { 
 constructor() { 
 console.log(this); 
 } 
} 
new Bus(); 
// ReferenceError: Must call super constructor in derived class 
// before accessing 'this' or returning from derived constructor 

 如果在派生类中显式定义了构造函数,则要么必须在其中调用 super(),要么必须在其中返回

一个对象。

class Vehicle {} 
class Car extends Vehicle {} 
class Bus extends Vehicle { 
 constructor() { 
 super(); 
 } 
} 
class Van extends Vehicle { 
 constructor() { 
 return {}; 
 } 
} 
console.log(new Car()); // Car {} 
console.log(new Bus()); // Bus {} 
console.log(new Van()); // {} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值