持续对红宝书进行输出(五)

706 阅读11分钟

1. 对象、类与面向对象编程

  1. 内容
    1. 对象
    2. 对象的创建过程
    3. 继承

1.1 理解对象

  1. 创建自定义对象的方式
    1. 通过new Object创建Object的一个新实例
    2. 通过对象字面量创建
// 通过new Object的方式创建对象
const person = new Object()
person.name = 'Nicholas'
person.age = 22
person.sayName = function(){
  return console.log(this.name);
}

// 通过字面量的方式创建对象
const Animal = {
  name: 'erha',
  age: 3,
  sayName(){
    console.log(this.name);
  }
}

1.1.1 属性的类型

  1. ECMAScript使用一些内部特性来描述属性的特征。开发者不能再JavaScript中直接访问这些特性。为了将某个特性标识为内部特性,规范会用两个中括号把特性的名称括起来,[[Enumerable]]

  2. 属性分两种,数据属性和访问器属性

    1. 数据属性:包含一个保存数据值的位置,值会从这个位置读取,也会写入到这个位置。数据属性4个特性描述它们的行为。
      1. [[Configurable]] 表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性这个特性都是true.
      2. [[Enumberable]] 表示属性是否可以通过for-in循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都为true
      3. [[Writable]] 表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的这个特性都是true.
      4. [[value]] 包含属性实际的值,这个特性默认是undefined
    2. 访问器属性不包含数据值。相反,它们包含一个获取(getter)函数和一个设置(setter)函数。在读取访问器属性时,会调用获取函数,在写入访问器属性时,会调用设置函数并传入新值。访问器有4个特性描述它们的行为。
      1. [[Configurable]] 表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性、以及是否可以把它改为数据属性。默认情况下,所有直接定义在对象上的属性的这个特性都是true
      2. [[Enumberable]] 表示属性是否可以通过for-in循环返回,默认情况下,所有直接定义在对象上的属性的这个特性都是true
      3. [[Get]] 获取函数,在读取属性时调用。默认值为undefined
      4. [[Set]] 设置函数,在写入属性时调用。默认值为undefined
  3. 要修改属性的默认特性,就必须使用Object.defineProperty()方法,这个方法接收3个参数:要给其添加属性的对象、属性的名称、描述符对象。描述符对象上的属性可以包含:configurable enumberable writable value。

let person = {}

// 使用defineProperty添加一个只读的属性
Object.defineProperty(person,'name',{
  configurable: false,
  wirtable: false,
  value: 'Nicholas'
})

console.log(person.name); // Nicholas
// 已经设置了属性的默认特性 不可删除,不可修改
delete person.name
person.name = 'Greg'
console.log(person.name); // Nicholas

// 当一个属性被定义为不可配置之后,就不能变回可配置的了,再次调用defineProperty()并修改configurable属性会报错

Object.defineProperty(person,'name',{ // TypeError: Cannot redefine property: name
  configurable: true
})
let book = {
  year_: 2017,
  edition: 1,
};

Object.defineProperty(book, "year", {
  configurable: true,
  enumerable: true,
  get() {
    return this.year_;
  },
  set(newValue) {
    if(newValue > 2017) {
      this.year_ = newValue;
      this.edition += newValue - 2017
    }
  },
});
book.year_ = 2018
console.log(book.edition); // 1

// 只定义了获取函数意味着属性是只读的,只定义了设置函数的属性是不能读取的,非严格模式下读取会返回undefined

1.1.2 定义多个属性

  1. 使用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
      }
    }
  }
})
console.log(book.edition); // 1
console.log(book.year);  // 2017

1.1.3 读取属性的特性

  1. 使用Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符。这个方法接收两个参数:属性所在的对象和要取得其描述符的属性名。
  2. Object.getOwnPropertyDescriptors()静态方法,这个方法实际上会在每个自有属性上调用Object.definedPropertyDesctiptor()并在一个新对象中返回它们。
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
      }
    }
  }
})

// 通过Object.getOwnPropertyDescriptor()方法取指定属性的属性描述符
let dpdb = Object.getOwnPropertyDescriptor(book,"year_")
console.log(dpdb.value); // 2017
console.log(dpdb.configurable); // false
console.log(typeof dpdb.get); // 'undefined'

// Object.getOwnPropertyDescriptors()静态方法
console.log(Object.getOwnPropertyDescriptors(book));
// {
//   year_: {
//     value: 2017,
//     writable: false,
//     enumerable: false,
//     configurable: false
//   },
//   edition: { value: 1, writable: false, enumerable: false, configurable: false },
//   year: {
//     get: [Function: get],
//     set: [Function: set],
//     enumerable: false,
//     configurable: false
//   }
// }

1.1.4 合并对象

  1. 就是把源对象所有的属性一起复制到目标对象上。
  2. Object.assign()方法,这个方法接收一个目标对象和一个或多个源对象作为参数,然后将每个源对象中可枚举和自有的属性复制到目标对象。
    1. 可枚举(Object.propertyIsEnumerable()返回true
    2. 自有(Object.hasOwnProperty()返回true)
  3. 对每个符合条件的属性,这个方法会使用源对象上的Get取得属性的值,然后使用目标对象上的Set设置属性的值。
let dest,src,result

dest = {}
result = { id: 'src' }

result = Object.assign(dest,result)
// Object.assign修改目标对象,也会返回修改后的目标对象
console.log(result === dest); // true
console.log(dest === src); // false
console.log(dest); // { id: 'src' }
console.log(result); // { id: 'src' }

/**
 * 多个源对象
 * 
 */

dest = {}
result = Object.assign(dest,{a: 'foo'},{b: 'bar'})
console.log(result); // { a: 'foo', b: 'bar' }
console.log(dest); // { a: 'foo', b: 'bar' }

/**
 * 获取函数和设置函数
 */
dest = {
  set a(val){
    console.log(val);
  }
}
src = {
  get a(){
    return 'foo'
  }
}
Object.assign(dest,src)
// 调用src的获取方法,调用dest的设置方法并传入参数'foo',因为这里的设置函数不执行赋值操作,所以实际上并没有把值转移过来
console.log(dest); // { a: [Setter] }

  1. Object.assgin()实际上对每个源对象执行的是浅复制。如果多个源对象都有相同的属性,则使用最后一个复制的值。
let dest,src,result

/**
 * 覆盖属性
 */
dest = { id: 'src' }
result = Object.assign(dest,{id: 'src1', a: 'foo'},{ id: 'src2', b: 'bar' })
console.log(dest); // { id: 'src2', a: 'foo', b: 'bar' }
console.log(dest === result); // true

/**
 * 可以通过目标对象上的设置函数观察到覆盖的过程
 */
dest = {
  set id(x){
    console.log(x);
  }
}
Object.assign(dest,{ id: 'first' },{ id: 'second' }, { id: 'third' })
// first
// second
// third
console.log(dest); // { id: [Setter] }

/**
 * 对象引用
 */
dest = {}
src = { a: {} }

Object.assign(dest,src)

console.log(dest); // { a: {} }
console.log(dest.a === src.a); // true

1.1.5 对象标识及相等判断

  1. 在ECMAScript6 之前,有些特殊情况即使是 === 操作符也无能无力:如
console.log(true === 1); // false
console.log({} === {}); // false
console.log('2' === 2); // false

// 这些情况在不同的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
  1. 为了改善上述的情况,ECMAScript6 规范新增了Object.is(),这个方法与 === 很像,但同时也考虑到了边界的情形。这个方法必须接收两个参数。
console.log(Object.is(true, 1)); // false
console.log(Object.is({}, {})); // false
console.log(Object.is("2", 2)); // false

// 正确的 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

1.1.6 增强的对象语法

  1. 属性值简写

let name = 'Matt'
let person = {
  name: name
}

console.log(person); // { name: 'Matt' }
// 简写属性名只要使用变量名就会自动被解释为同名的属性键。如果没有找到同名变量,则会抛出ReferenceError

let person1 = {
  name
}
console.log(person1); // { name: 'Matt' }
  1. 可计算属性
    1. 在引入可计算属性之前,如果想使用变量的值作为属性,那么必须先声明对象,然后使用中括号语法来添加属性。(也就是不能在对象字面量中直接动态命名属性)

const nameKey = 'name'
const ageKey = 'age'
const jobKey = 'job'

let person = {}
person[nameKey] = 'Nicholas'
person[ageKey] = 30
person[jobKey] = 'singer'
console.log(person); // { name: 'Nicholas', age: 30, job: 'singer' }

// 有了可计算属性,就可以在对象字面量中完成动态属性赋值
let person1 = {
  [nameKey]: 'Matt',
  [ageKey]: '20',
  [jobKey]: 'Engineer'
}
console.log(person1); // { name: 'Matt', age: '20', job: 'Engineer' }

// 因为被当作JavaScript表达式求值,所以可计算属性本身可以是复杂的表达式,在实例化时求值

let count = 0
function getCount(key){
  return `${key}_${count++}`
}

let person2 = {
  [getCount(nameKey)]: 'Bob',
  [getCount(ageKey)]: 31,
  [getCount(jobKey)]: 'doctor'
}
console.log(person2); // { name_0: 'Bob', age_1: 31, job_2: 'doctor' }
  1. 简写方法名

let person = {
  sayName: function(name){
    console.log(`My name is ${name}`);
  }
}

person.sayName('Matt') // My name is Matt

// 使用简写方法名
let person1 = {
  sayName(name){
    console.log(`My name is ${name}`);
  }
}
person1.sayName('xiaoming') // My name is xiaoming

/**
 * 简写方法名与可计算属性键相互兼容
 */
const methodKey = 'sayName'

let person2 = {
  [methodKey](name){
    console.log(`My name is ${name}`);
  }
}

person2.sayName('Bob') // My name is Bob

1.1.7 对象解构

  1. ECMAScript6新增了对象解构语法,可以在一条语句中使用嵌套数据实现一个或多个赋值操作。简单的说,对象解构就是使用与对象匹配结构的方式为对象属性赋值。
// 不适用对象结构进行赋值操作
let person = {
  name: "Nicholas",
  age: 22,
};
let personName = person.name,
  personAge = person.age;
console.log(personName, personAge); // Nicholas 22

// 使用解构赋值的方式进行赋值
let { name: personName1, age: personAge1 } = person;
console.log(personName1, personAge1); // Nicholas 22

/**
 * 使用解构,可以在一个类似对象字面量的结构中,声明多个变量,同时执行多个赋值操作。
 * 如果想让变量直接使用属性的名称,那么可以使用简写语法。
 */
let { name, age } = person;
console.log(name, age); // Nicholas 22

/**
 * 解构赋值不一定与对象的属性匹配,赋值的时候可以忽略某些属性,而如果引用的属性不存在
 * 则该变量的值就是 undefined
 */
let student = {
  nickName: "snake",
  age1: 27,
};
let { job } = student;
console.log(job); // undefined

/**
 * 可以在解构赋值的同时定义默认值
 */
let { nickName, age1, weight='60kg' } = student
console.log(weight); // 60kg
  1. 解构在内部使用函数 ToObject() 把源数据解构转换为对象。这意味着在对象解构的上下文中,原始值会被当成对象。这也意味着nullundefined不能被解构,否则会抛出异常。
let { length } = 'foobar'
console.log(length); // 6

let { constructor: c } = 4
console.log(c); // [Function: Number]
console.log(c == Number); // true

let { _ } = null // 报错
let { _ } = undefined // 报错

// 解构并不要求变量必须在解构表达式中声明,不过。如果是给事先声明的变量赋值,则赋值表达式必须包含在一对括号中

let personName, personAge

let person = {
  name: 'Nicholas',
  age: 22
};

({name: personName, age: personAge} = person)
console.log(personName,personAge); // Nicholas 22

  1. 嵌套解构 解构对于引用嵌套结构的属性或赋值目标没有限制。为此,可以通过解构来复制对象属性:
let person = {
  name: "Nicholas",
  age: 20,
  job: {
    title: "Engineer",
  },
};

let copyPerson = {};

({ name: copyPerson.name, age: copyPerson.age, job: copyPerson.job } = person);

console.log(copyPerson); // { name: 'Nicholas', age: 20, job: { title: 'Engineer' } }

person.job.title = "Cookier";
// 因为对象的一个引用被赋值给了copyPerson,所以修改person.job对象的属性也会影响到copyPerson
console.log(copyPerson); // { name: 'Nicholas', age: 20, job: { title: 'Cookier' } }

/**
 * 解构赋值可以使用嵌套结构,以匹配嵌套的属性
 */

let { job: { title } } = person;
console.log(title); // Cookier

  1. 部分解构
    1. 涉及多个属性的解构赋值时一个输出无关的顺序化操作。如果一个解构表达式涉及多个赋值,开始的赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分:
let person = {
  name: "Matt",
  age: 20,
};

let personName, personAge, personBar;

try {
  ({
    name: personName,
    foo: { bar: personBar },
    age: personAge,
  } = person);
} catch (error) {}

console.log(personName, personAge, personBar); // Matt undefined undefined

  1. 参数上下文匹配
    1. 在函数参数列表中也可以进行解构赋值。对参数的解构赋值不会影响arguments对象,但可以在函数签名中声明在函数体内使用局部变量。
let person = {
  name: "Matt",
  age: 20,
};

function printPerson(foo,{name,age},bar){
  console.log(arguments);
  console.log(name,age);
}

printPerson('first',person,'third')
// [Arguments] {
//   '0': 'first',
//   '1': { name: 'Matt', age: 20 },
//   '2': 'third'
// }
// Matt 20

1.2 创建对象

  1. 虽然使用Object构造函数或对象字面量可以方便地创建对象,但这些方式也有明显不足;创建具有同样接口的多个对象需要重复编写很多代码。

1.2.1 工厂模式

  1. 用于抽象创建特定对象的过程。
// 工厂函数
function createPerson(name, age, sex) {
  let o = new Object();
  o.name = name;
  o.age = age;
  o.sex = sex;
  o.sayName = function(){
    console.log(this.name);
  }
  return o;
}

//
let person1 = createPerson("Nicholas", 21, "male");
let person2 = createPerson("Angle", 22, "female");
console.log(person1); // { name: 'Nicholas', age: 21, sex: 'male', sayName: [Function (anonymous)]}
console.log(person2); // { name: 'Nicholas', age: 21, sex: 'male', sayName: [Function (anonymous)]}

/**
 *  函数createPerson()接收3个参数,根据这几个参数构建一个包含Person信息的对象。可以用不同的参数调用这个函数
 *  每次调用都会返回3个属性和1个方法的对象。
 *  这种工厂模式函数虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即创建的对象是什么类型)
 */

1.2.2 构造函数模式

  1. ECMAScript中的构造函数是用于创建特定类型对象的。像Object和Array这样的原生构造函数,运行时可以直接在执行环境中使用。也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。
// 构造函数
function Person(name, age, sex) {
  this.name = name;
  this.age = age;
  this.sex = sex;
  this.sayName = function () {
    console.log(this.name);
  };
}

// 生成实例对象
let person1 = new Person("Nicholas", 21, "male");
let person2 = new Person("Angle", 22, "female");

person1.sayName() // Nicholas
person2.sayName() // Angle

/**
 *  Person()构造函数代替了createPerson()工厂函数。实际上,Person()内部的代码跟createPerson()工厂函数是一样的,只是有一点区别
 *  1. 没有显示的创建对象
 *  2. 属性和方法直接赋值给了this
 *  3. 没有return
 *  4. 函数名首字母大写了
 */
  1. 构造函数中首字母大写,有助于ECMAScript中区分构造函数和普通函数。毕竟ECMAScript的构造函数就是创建对象的函数。
  2. 要创建Person的实例,应使用new操作符。以这种方式调用构造函数会执行如下操作。
    1. 在内存中创建一个新对象
    2. 这个新对象内部的[[prototype]]特性被赋值为构造函数的prototype属性
    3. 构造函数内部的this被赋值为这个新对象(即this指向新对象)
    4. 执行构造函数内部的代码(给新对象添加属性)
    5. 如果构造函数返回非空对象,则返回该对象。否则,返回刚创建的新对象
// 构造函数
function Person(name, age, sex) {
  this.name = name;
  this.age = age;
  this.sex = sex;
  this.sayName = function () {
    console.log(this.name);
  };
}

// 生成实例对象
let person1 = new Person("Nicholas", 21, "male");
let person2 = new Person("Angle", 22, "female");

// person1 和 person2 分别保存着Person的不同实例。这两个对象都有一个constructor属性指向Person

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

/**
 *  constructor本来是用于标识对象类型的。一般认为instanceof操作符是确定对象类型更可靠的方式
 *  如 person1 和 person2 对象都是Object的实例,同时也是Person的实例 
 */
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true

  1. 定义自定义构造函数可以确保实例被标识为特定类型,在以上这个例子中 person1和person2之所以被认为是Object的实例,是因为所有自定义对象都继承自Object。
  2. 构造函数不一定要写成声明式的形式。赋值给变量的函数表达式也可以表示构造函数。
// 构造函数
let Person =  function (name, age, sex) {
  this.name = name;
  this.age = age;
  this.sex = sex;
  this.sayName = function () {
    console.log(this.name);
  };
}
  1. 在实例化对象时,如果不想传参,那么构造函数后面的括号可加可不加。只要有new操作符,就可以调用相应的实例。
// 构造函数
function Person(name, age, sex) {
  this.name = name;
  this.age = age;
  this.sex = sex;
  this.weight = '60kg'
  this.sayName = function () {
    console.log(this.weight);
  };
}

// 实例化对象时,构造函数后面的括号可加可不加
let person1 = new Person()
let person2 = new Person;

person1.sayName() // 60kg
person2.sayName() // 60kg
  1. 构造函数也是函数
    1. 构造函数和普通函数的唯一区别就是调用方式不同。除此之外,构造函数也是函数,并没有把某个函数定义为构造函数的特殊语法。
    2. 任何函数只要使用了 new 操作符调用就是构造函数,而不使用 new 操作符调用的函数就是普通函数。
      // 构造函数
      function Person(name, age, sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
        this.weight = "60kg";
        this.sayName = function () {
          console.log(this.name);
        };
      }
      // 1. 作为构造函数
      let person1 = new Person('Nicholas',31,'male')
      person1.sayName() // Nicholas
      // 2. 作为函数调用
      Person('Matt',32,'female')
      window.sayName() // Matt
      // 3. 在另一个对象的作用域中调用
      let o = new Object()
      Person.call(o, "Kristen",33,"female")
      o.sayName() // Kristen
      /*
       *   方式一:使用new 构造函数的方式创建实例对象
           方式二:使用普通函数的调用方式,这时候没有使用new操作符调用Person(),结果会将属性和方法添加到window对象。
                  在调用一个函数没有明确设置this的情况下,this始终指向Global()对象,在浏览器中就是window对象
           方式三:通过call()调用函数,同时将特定对象指定为作用域。这里的调用将对象 o 指定为Person内部this的值。
      */
  1. 构造函数的问题
    1. 构造函数的主要问题在于,其定义的方法会在每个实例上都创建一遍。因此对于前面的例子而言,person1和person2都有名为sayName()的方法,但这两个方法不是同一个Function()的实例。
    2. ECMAScript中的函数是对象,因此每次定义函数时,都会初始化一个对象。
// 构造函数
function Person(name, age, sex) {
  this.name = name;
  this.age = age;
  this.sex = sex;
  // 其实构造函数中的方法应该是这样的
  this.sayName = new Function("console.log(this.name)")
}

/**
 *  这样理解这个构造函数可以更清楚地知道,每个Person实例都会有自己的Function实例用于显示name属性
 *  当然,这种方式创建函数会带来不同作用域链和标识符解析。但创建新Function实例的机制是一样的。因此
 *  不同实例上的函数虽然同名但不相等。
 */
 let person1 = new Person('Nicholas',31,'male')
 let person2 = new Person('Matt',31,'female')

 console.log(person1.sayName == person2.sayName); // false

 /**
  *  this对象可以把函数与对象的绑定推迟到运行时。所以,把函数定义转移到构造函数外部
  * 
  */
  function Person2(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
    // sayName属性只是一个包含指向外部的指针
    this.sayName = sayName
  }
  function sayName(){
    console.log(this.name);
  }

  let person3 = new Person2('Nicholas',31,'male')
  let person4 = new Person2('Matt',31,'female')

  /**
   *  因为sayName只是一个包含指向外部的指针,所以person3和person4共享了定义在全局作用域上的sayName()函数,
   *  虽然这样解决了逻辑上构造函数中不同实例上函数同名不相等的问题。但全局作用域也因此会被 搞乱。
   */
  console.log(person3.sayName == person4.sayName); // true

1.2.3 原型模式

  1. 每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处就是在它上面定义的属性和方法可以被对象实例共享。
// 通过函数定义的方式 在函数的prototype属性上添加属性和方法
function Person(){}

Person.prototype.name = 'Nicholas'
Person.prototype.age = 23
Person.prototype.sex = 'male'
Person.prototype.sayName = function(){
  console.log(this.name);
}

let person1 =  new Person()

person1.sayName() // Nicholas
let person2 = new Person()
person2.sayName() // Nicholas

console.log(person1.sayName == person2.sayName); // true

// 使用函数表达式的方法 

let Animal = function(){}
Animal.prototype.weight = '3kg'
Animal.prototype.pair = 'long'
Animal.prototype.eating = function(){
  console.log('cat');
}
let an1 = new Animal
let an2 = new Animal

console.log(an1.eating == an2.eating); // true
  1. 理解原型
    1. 无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype属性(指向原型对象),默认情况下,所有原型对象自动获得一个名为constructor的属性,指回与之关联的构造函数。
    2. 在自定义构造函数时,原型对象默认只会获取constructor属性。其他的所有方法都继承自Object。每次调用构造函数创建一个新实例,这个实例的内部[[prototype]]指针就会被赋值为构造函数的原型对象。
    3. 每个对象上暴露__proto__属性(隐式原型链),通过这个属性可以访问对象的原型。
// 通过函数定义的方式 在函数的prototype属性上添加属性和方法
function Person(){}

Person.prototype.name = 'Nicholas'
Person.prototype.age = 23
Person.prototype.sex = 'male'
Person.prototype.sayName = function(){
  console.log(this.name);
}

console.log(Person.prototype.constructor == Person); // true

/**
       *  构造函数可以是 函数表达式 也可以是 函数声明形式
       *  let Person = function(){}
       *  function Person(){}
      */
      function Person(){}
      /**
       *  声明之后,构造函数就有一个与之关联的prototype属性
      */
      console.log(typeof Person.prototype) // Object
      console.log(Person.prototype); // { constuctor: f Person(),__proto__ }
      /**
       * 构造函数有一个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__);
      /**
       * 构造函数、实例、原型对象 是三个完全不同的对象
      */
      let person1 = new Person
      console.log(Person == person1); // false
      console.log(Person.prototype == person1); // false
      /**
       * 实例通过__proto__连接到原型对象,它实际上指向隐藏特性[[prototype]]
       * 构造函数通过prototype属性链接到原型对象
       * 实例与构造函数没有直接联系 实例与原型对象有直接联系
      */
      console.log(person1.__proto__ === Person.prototype); // true
      console.log(person1.__proto__.constructor === Person); // true
      /**
       * 同一个构造函数创建的两个实例,共享同一个原型对象
      */
      let person2 = new Person()
      console.log(person1.__proto__ === person2.__proto__); // true

      /**
       * instanceof检查实例的原型链中是否包含指定构造函数的原型
      */
      console.log(person1 instanceof Person); // true
      console.log(person1 instanceof Object); // true
      console.log(person2 instanceof Person); // true
      console.log(Person.prototype instanceof Object); // true
4. 案例代码对应原型的流程图

image.png

1. 图中展示了Person构造函数、Person的原型对象和Person两个实例之间的关系。Person的prototype指向原型对象,而Person.prototype.constructor指回Person构造函数。
2. 原型对象包含constructor属性和其他后来添加的方法和属性
3. Person的两个实例 person1和person2都有一个内部属性(隐式原型属性__proto__)指回Person.prototype,而且两者都与构造函数没有直接的联系。
4. 虽然两个实例对象都没有属性和方法,但person1.sayName()可以正常调用,这是由于对象属性查找机制的原因(也就是通过原型链查找)

5. 虽然不是所有实现都对外暴露了[[prototype]],但可以使用 `isPropertyOf()`方法确定两个对象的这种关系。本质上,`isPropertyOf()`会在传入参数的[[prototype]]指向调用它的对象时返回true
      console.log(Person.prototype.isPrototypeOf(person1)); // true
      console.log(Person.prototype.isPrototypeOf(person2)); // true
  // 这里通过原型对象调用isPrototypeOf()方法检查person1和person2.因为 perosn1 和 person2
  // 内部都有链接指向Person.prototype,所以结果都返回true
6. ECMAScript的Object类型有一个方法叫 Object.getPrototypeOf(),返回参数的内部特性[[prototype]]的值。
      console.log(Person.getPrototypeOf(person1)); // true
      console.log(Person.getPrototypeOf(person1).name); // "Nicholas"
 // 通过Person.getPrototypeOf()返回的对象就是传入对象的原型对象,所以通过原型对象取出的name属性的值。
7. Object类型还有一个setPropertyOf()方法,可以向实例的私有特性[[prototype]]写入一个新值。这样就可以重写一个对象的原型链继承关系。
let biped = {
  num: 2
}
let Animal = {
  name: 'Bob'
}

Object.setPrototypeOf(Animal, biped)

console.log(Animal.name); // Bob
console.log(Animal.num); // 2
console.log(Object.getPrototypeOf(Animal) === biped); // true
// Object.setPropertyOf()可能回严重影响代码性能。
8. 为了避免使用Object.setPropertyOf()可能造成的性能下降,可以通过Object.create()来创建一个新对象,同时为其指定原型。
let biped = {
  num: 2
}
let animal = Object.create(biped)
animal.name = "Matt"

console.log(animal.name); // Matt
console.log(animal.num);  // 2

console.log(Object.getPrototypeOf(animal) === biped); // true
  1. 原型层级
    1. 虽然可以通过实例读取原型对象上的值,但不可能通过实例重写这些值。如果在实例上添加一个与原型对象中同名的属性,那就会在实例上创建这个属性,这个属性会遮住原型对象上的属性。
    2. 只要给对象实例上添加一个属性,这个属性就会遮蔽原型对象上的同名属性,虽然不会修改原型对象上的同名属性,但会屏蔽对它的访问。即使在实例上把这个属性设置为null,也不会恢复它和原型的联系。不过,使用delete操作符可以完全删除实例上的这个属性,从而让标识符解析过程能够继续搜搜原型对象。
// 通过函数定义的方式 在函数的prototype属性上添加属性和方法
function Person(){}

Person.prototype.name = 'Nicholas'
Person.prototype.age = 23
Person.prototype.sex = 'male'
Person.prototype.sayName = function(){
  console.log(this.name);
}

let person1 = new Person()
let person2 = new Person()

person1.name = "Matt"

console.log(person1.name); // Matt
console.log(person2.name); // Nicholas

/**
 *  在这个例子中,person1的name属性遮蔽了原型上的同名属性。虽然person1.name和person2.name都返回了值,
 *  但person1.name 返回的是“Matt” 来自实例。person2.name返回的是"Nicholas" 来自原型。
 *  当log person1.name,会先在实例上搜索这个属性。因为这个属性在实例身上,所以就不会搜索原型对象
 *  而log person2.name时,因为实例身上没有name,所以会继续搜索原型对象并使用定义在原型对象上的属性
 */

/**
 *  删除实例身上的属性,从而继续搜索原型对象
 */
delete person1.name

console.log(person1.name); // Nicholas  来自原型对象
console.log(person2.name); // Nicholas  来自原型对象
3. hasOwnProperty()方法用于确定某个属性是在实例上还是在原型对象上。这个方法继承Object,会在属性存在于调用它的对象实例上时返回true
// 通过函数定义的方式 在函数的prototype属性上添加属性和方法
function Person(){}

Person.prototype.name = 'Nicholas'
Person.prototype.age = 23
Person.prototype.sex = 'male'
Person.prototype.sayName = function(){
  console.log(this.name);
}

let person1 = new Person()
let person2 = new Person()

console.log(person1.hasOwnProperty("name")); // false

person1.name = "Matt"
console.log(person1.name); // 来自实例 
console.log(person1.hasOwnProperty("name")); // true

console.log(person2.name); // 来自原型
console.log(person2.hasOwnProperty("name")); // false
  1. 原型和in操作符
    1. 单独使用 in操作符会在可以通过对象访问指定属性时返回true, 无论该属性是在实例上还是原型上。
    2. for in循环中使用
    3. 只要使用in 返回true,且使用 hasOwnProperty()返回 false,说明该属性是存在与原型对象上。
// 通过函数定义的方式 在函数的prototype属性上添加属性和方法
function Person(){}

Person.prototype.name = 'Nicholas'
Person.prototype.age = 23
Person.prototype.sex = 'male'
Person.prototype.sayName = function(){
  console.log(this.name);
}

let person1 = new Person()
let person2 = new Person()

console.log(person1.hasOwnProperty("name")); // false
console.log("name" in person1); // true

person1.name = "Matt"
console.log(person1.hasOwnProperty("name")); // 来自实例
console.log("name" in person1); // true

console.log(person2.hasOwnProperty("name")); // 来自原型
console.log("name" in person2); // true

// 判断属性在原型对象上的函数
function hasPrototypeProperty(object,name){
    return !object.hasOwnProperty(name) && (name in object)
}
4. 在fot-in循环中使用 in 操作符时,可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性。屏蔽原型中不可枚举([Enumberable]特性被设置为false)属性的实例属性也会在for-in循环中返回,因为默认情况下开发者定义的属性都是可枚举的。
5. 要获得对象上所有可枚举的实例属性,可以使用Object.keys()方法,这个方法接收一个对象作为参数,返回包含该对象所有可枚举属性名称的字符串数组。
6. 如果想列出所有实例属性,无论是否可枚举,都可以使用Object.getOwnPropertyNames()
7. ECMAScript6 新增符号类型后,添加了一个Object.getOwnPropertySymbols()方法,与Object.getOwnPropertyNames()类似,只是针对符号
// 通过函数定义的方式 在函数的prototype属性上添加属性和方法
function Person() {}

Person.prototype.name = "Nicholas";
Person.prototype.age = 23;
Person.prototype.sex = "male";
Person.prototype.sayName = function () {
  console.log(this.name);
};

let person1 = new Person();

let keys = Object.keys(Person.prototype);
console.log(keys); // [ 'name', 'age', 'sex', 'sayName' ]

let person2 = new Person();
person2.name = "Rob";
person2.age = 32;
let keys2 = Object.keys(person2);
console.log(person2); // Person { name: 'Rob', age: 32 }

// 使用Object.getOwnPropertyNames() 列出所有实例属性
let keys3 = Object.getOwnPropertyNames(Person.prototype);
console.log(keys3); // [ 'constructor', 'name', 'age', 'sex', 'sayName' ]

/**
 *  Object.getOwnPropertySymbols() 方法
 *
 */
let k1 = Symbol["k1"],
  k2 = Symbol["k2"];
let o = {
  [k1]: "k1",
  [k2]: "k2",
};

console.log(Object.getOwnPropertySymbols(o)); // [Symbol[k1], Symbol[k2]]

  1. 属性枚举的顺序
    1. for-in循环、Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()、Object.assign()在属性枚举方面是有很大差别的。
    2. for-in循环和Object.assign() 的枚举顺序是不确定的。

1.2.4 对象迭代

  1. ECMAScript2017 新增了两个静态方法,用于将对象内容转换为序列化的--可迭代的--格式。
    1. Object.values() 返回对象值数组
    2. Object.entries() 返回对象键值对数组
let o = {
  foo: 'bar',
  baz: 1,
  qux: []
}
console.log(Object.values(o)); // [ 'bar', 1, [] ]
console.log(Object.entries(o)); // [ [ 'foo', 'bar' ], [ 'baz', 1 ], [ 'qux', [] ] ]

// 非字符串属性会被转换为字符串输出,这两个方法执行对象的浅复制
console.log(Object.values(o)[0] === o.foo); // true

// 符号属性会被忽略
  1. 其他原型语法
    1. 直接通过一个包含所有属性和方法的对象字面量来重写原型。
function Person(){}

Person.prototype = {
  name: 'Nicholas',
  age: 22,
  job: 'Software',
  sayName(){
    console.log(this.name);
  }
}
/**
 *  1,在这个例子中Person.protorype被设置为通过对象字面量创建的新对象,最终结果和直接在
 *  构造函数原型上添加属性和方法一样。
 *  2,问题:这样重写之后,Person.prototype的constructor属性就不指向Person了
 *  3. 上面的写法完全重写了默认的prototype对象,因此其constructor属性也指向了
 *  完全不同的新对象(Object构造函数),不在指向原来的构造函数。虽然instanceof操作符还能
 *  可靠的返回值,但我们不能再依靠constructor属性来识别类型了
 */
let friend = new Person()

console.log(friend instanceof Person); // true
console.log(friend instanceof Object); // true

// constructor属性现在等于Object 而不是Person
console.log(friend.constructor == Person);
console.log(friend.constructor == Object);

/**
 *  如果constructor属性的值很重要,可以再重写原型对象时专门设置 它的值。
 *  需要注意这样直接设置constructor属性值的方式,属性会创建一个[[Enumberable]]为true的属性
 *  而原生constructor属性默认是不可枚举的。因此使用Object.definedPropety()能更改重写的
 *  constructor属性
 */

function Animal(){}

Animal.prototype = {
  constructor: Animal,
  nikeName: 'snake',
  age: 3,
  job: 'eating',
  sayName(){
    console.log(this.nikeName);
  }
}

let snake = new Animal()
// 重写构造函数原型对象时,设置了constructor属性,让构造函数原型的constructor属性指向回构造函数
console.log(snake.constructor == Animal); // true
  1. 原型的动态性
    1. 因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改也会在实例上反映出来。
    2. 虽然随时能给原型添加属性和方法,并能够立即反映在所有对象实例上,但这跟重写整个原型是两回事。实例的[[prototype]]指针是在调用构造函数时自动赋值的。这个指针即使把原型修改为不同的对象也不会变。重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型。实例只有指向原型的指针,没有指向构造函数的指针。

image.png

function Person(){}

Person.prototype = {
  name: 'Nicholas',
  age: 22,
  job: 'Software',
  sayName(){
    console.log(this.name);
  }
}
/**
 * 任何时候对原型对象所做的修改也会在实例上反映出来 
 */
let friend = new Person()

Person.prototype.sayHi = function(){
  console.log("hi");
}
friend.sayHi() // hi

function Person(){}
// 重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型
let friend = new Person()
Person.prototype = {
  name: 'Nicholas',
  age: 22,
  job: 'Software',
  sayName(){
    console.log(this.name);
  }
}

friend.sayName() // 错误
  1. 原型的问题
    1. 弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值。
    2. 原型上的所有属性是在实例上共享的。
    3. 可以通过在实例上添加同名属性来简单地遮蔽原型上的属性。真正的问题来自包含引用值的属性。

function Person(){}

Person.prototype = {
  constructor: Person,
  name: 'Nicholas',
  age: 22,
  job: 'Software Engineer',
  friends: ['Shelby','Court']
}

let person1 = new Person()
let person2 = new Person()

person1.friends.push('Ant')
console.log(person2.friends); // [ 'Shelby', 'Court', 'Ant' ]
console.log(person1.friends === person2.friends); // true

/**
 *  person1.friends中push一个字符串数组,会影响到person2.friends
 *  一般来说,不同的实例应该有属于自己的属性副本
 */

1.3 继承

  1. 实现继承是ECMAScript唯一支持继承的方式,而这主要是通过原型链实现的。

1.3.1 原型链

  1. ECMAScript-262 把 原型链定义为ECMAScript的主要继承方式。其基本思想就是通过原型继承多个引用类型的属性和方法。
  2. 重温构造函数、原型和实例的关系
    1. 每个构造函数都有一个原型对象
    2. 原型有一个属性指回构造函数
    3. 实例有一个内部指针指向原型
  3. 如果原型是另一个对象的实例
    1. 那就意味着这个原型本身内部指针指向另一个原型,相应地另一个原型对象也有一个指针指向另一个构造函数。
    2. 这样就在实例和原型之间构造了一条原型链。

function SuperType(){
  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.property
}

let instance = new SuperType()

console.log(instance.getSuperValue()); // true

/**
 *  以上代码定义了两个类型 SuperType和SubType。这两个类型分别定义了一个属性和一个方法。
 *  这两个类型的主要区别是SubType通过创建SubperType的实例并将其赋值给自己的原型(SubType.prototype)实现了对SubperType的继承
 *  这个赋值重写了SubType最初的原型,将其替换为SuperType的实例。 
 *  着意味着SuperType实例可以访问的所有属性和方法也会存在于SuperType的prototype
 *  这样实现继承之后,代码紧接着又给SubType.prototype(也就是这个SuperType的实例)添加了一个新方法。
 *  最后又创建了SubType的实例并调用它继承的getSuperValue()方法。
 */

image.png

1. 这个例子中实现继承的关键,是SubType没有使用默认原型,而是将其替换成一个新的对象。这个新的对象恰好是SuperType的实例。这样一来,SubType的实例不仅能从SuperType的实例中继承属性和方法,而且还与SuperType的原型进行挂钩。
2. 于是instance指向SubType.prototype,而SubType.prototype指向SuperType.prototype
3. getSuperValue()方法还在SuperType.prototype对象上,而property是一个实例属性。
4. 由于SubType.prototype的constructor属性被重写为指向SuperType,所以instance.constructor也指向SuperType4. 调用instance.getSuperValue()经过了3个步骤:instance、SubType.prototypeSuperType.prototype

4. 默认原型

1. 认情况下,所用引用类型都继承自Object,这也是通过原型链实现的。
2. 任何函数的默认原型都是一个Object的实例,这意味着这个实例有一个内部指针指向Object.prototype

image.png

  1. 原型与继承关系
    1. 原型和实例的关系可以通过两种方式来确定。
    2. 第一种方式是使用instanceof操作符,如果一个实例的原型链中出现过相应的构造函数,则instanceof返回true
    3. 第二种方式是使用isPrototypeOf()方法,原型链中的每个原型都可以调用这个方法,只要原型链中包含这个原型,这个方法就返回true.
console.log(instance instanceof SuperType); // true
console.log(instance instanceof Object);  // true

console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(SuperType.prototype.isPrototypeOf(instance)); // true
  1. 关于方法
    1. 子类有时候需要覆盖父类的方法,或者增强父类没有的方法,为此,这些方法必须在原型赋值之后再添加到原型上。
    2. 以对象字面量方式创建原型方法会破坏之前的原型链,因为这相当于重写了原型链。

function SuperType(){
  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

function SuperType(){
  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
  }
}

// 子类的原型在被赋值为SuperType的实例后,又被一个对象字面量覆盖了。覆盖后的原型是一个Object的实例,而不再是SuperType的实例。

  1. 原型链的问题
    1. 当原型中包含引用值的时候,这些引用值会在实例间共享,这也是为什么属性通常会在构造函数中定义而不会定义在原型上的原因。
    2. 子类型在实例化时不能给父类型的构造函数传参。
function SuperType(){
  this.colors = ['red','white','blue']
}

function SubType(){}

SubType.prototype = new SuperType()

let cols1 = new SubType()
cols1.colors.push('black')

console.log(cols1.colors); // [ 'red', 'white', 'blue', 'black' ]

let cols2 = new SubType()
// SubType的实例都会共享colors属性
console.log(cols2.colors); // [ 'red', 'white', 'blue', 'black' ]

1.3.2 盗用构造函数

  1. 为了解决原型包含引用值导致的问题,使用盗用构造函数的基本思路是,在子类构造函数中调用父类构造函数。因为函数就是在特定上下文中执行代码的简单对象,所以可以使用apply()和call()方法以新创建的对象为上下文执行构造函数。
function SuperType(){
  this.colors = ['red','white','blue']
}

function SubType(){
  // 继承SuperType
  SuperType.call(this)
}

let person1 = new SubType()
person1.colors.push('olivedrab')
console.log(person1.colors); // [ 'red', 'white', 'blue', 'olivedrab' ]

let person2 = new SubType()
console.log(person2.colors); // [ 'red', 'white', 'blue' ]

/**
 *  示例代码中,继承SuperType 这里展示了盗用构造函数。通过使用call()或apply()方法,SuperType构造函数在为
 *  SubType的实例创建的新对象上下文中执行了,这相当于新的SubType对象运行了SuperType()函数中的所有初始化代码
 *  结果就是每个实例都会有自己的colors属性。
 */
  1. 传递参数 相比于使用原型链,盗用构造函数的一个优点就是可以在子类构造函数中向父类构造函数传参。

function SuperType(name){
  this.colors = ['red','white','blue']
  this.name = name
}

function SubType(){
  // 继承SuperType 并传参
  SuperType.call(this,"Nicholas")

  // 实例属性
  this.age = 29
}

let person1 = new SubType()
console.log(person1.name); // Nicholas
console.log(person1.age);  // 29

/**
 *  在这个例子中,SuperType构造函数接收一个参数name,然后将它赋值给一个属性。
 *  在SubType构造函数中调用SuperType构造函数时传入这个参数,实际上会在SubType实例
 *  定义name属性
 */
  1. 盗用构造函数的问题
    1. 盗用构造函数的主要缺点,也是使用构造函数模式自定义类型的问题:
    2. 必须在构造函数中定义方法,因此函数不能复用
    3. 子类不能访问父类原型上定义的方法。

1.3.3 组合继承

  1. 组合继承(伪经典继承)综合了原型链和盗用构造函数,将两者的优点集中起来。基本思路是使用原型链继承原型上的方法和属性。而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现服用,又可以让每个实例都有自己的属性。
// 组合继承
function SuperType(name) {
  this.colors = ["red", "white", "blue"];
  this.name = name;
}
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 person1 = new SubType("Nicholas", 22);
person1.colors.push("olivedrab");
console.log(person1.colors); // [ 'red', 'white', 'blue', 'olivedrab' ]
person1.sayName() // Nicholas
person1.sayAge() // 22

let person2 = new SubType("Matt", 23);
console.log(person2.colors);
person2.sayName() // Nicholas
person2.sayAge() // 23

/**
 *  在这个例子中,SuperType构造函数定义了两个属性,name和colors,而它的原型上也定义了一个方法sayName()
 *  SubType()构造函数调用了SuperType构造函数,传入了 name参数,然后定义了自己的属性age.
 *  此外,SubType.prototype也被赋值为 SuperType的实例。原型赋值之后,又在这个原型上添加了新方法sayAge()
 *  这样就可以创建两个SubType新实例,让两个实例都有自己的属性,同时还共享相同的方法。
 */

1.3.4 原生式继承

  1. 即使不自定义类型也可以通过原型实现对象之间的信息共享。
  2. 通过一个函数创建临时构造函数,将传入的对象赋值给这个构造函数的原型,然后返回这个临时构造函数的实例。

// 原生式继承
// 
/**
 *  定义一个函数接收一个对象参数, 该函数内部会创建一个临时的构造函数,将这个临时构造函数的原型 等于 传入的对象参数
 *  然后返回这个临时构造函数的实例对象
 *  本质上定义的这个函数object() 是对传入对象参数 进行了一次浅复制
 * 
 *  原生式继承: 适用于,有一个对象,想在它的基础上再创建一个新对象。这样就需要把这个对象先传给object(),然后再对返回的
 *  对象进行适当的修改。
 */
function object(obj){
  function Func(){}
  Func.prototype = obj
  return new Func()  
}

let person = {
  name: 'Nicholas',
  friends: ['Shelby','Van']
}

let anotherPerson = object(person)
anotherPerson.name = 'Matt'
anotherPerson.friends.push('Greg')

console.log(anotherPerson.friends); // [ 'Shelby', 'Van', 'Greg' ]

let yetAnotherPerson = object(person)
yetAnotherPerson.name = 'Bob'
yetAnotherPerson.friends.push('Angle')

console.log(yetAnotherPerson.friends); // [ 'Shelby', 'Van', 'Greg', 'Angle' ]

  1. ECMAScript5通过增加Object.create()方法将原型式继承的概念规范化了,这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(可选)
let person = {
  name: 'Nicholas',
  friends: ['Nicholas','Shelby']
}

// ES5 使用Object.create()规范了原型式继承的概念
let person1 = Object.create(person)
person1.name = 'Matt'
person1.friends.push('Bob')
console.log(person1.friends); // [ 'Nicholas', 'Shelby', 'Bob' ]

let person2 = Object.create(person)
person2.name = 'Angle'
person2.friends.push('Neti')

console.log(person2.friends); // [ 'Nicholas', 'Shelby', 'Bob', 'Neti' ]
  1. Object.create()的第二个参数与Object.definedProperties()的第二个参数一样:每个新增属性都通过各自的描述符来描述。以这种方式添加的属性会遮蔽原型对象上的同名属性。
let person = {
  name: 'Nicholas',
  friends: ['Nicholas','Shelby']
}

// ES5 使用Object.create()规范了原型式继承的概念
let person1 = Object.create(person,{
  name: {
    value: 'Angle'
  }
})
console.log(person1.name); // Angle
  1. 原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但是要注意,属性中包含的引用值始终会在相关实例对象间共享,跟使用原型模式一样。

1.3.5 寄生式继承

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

// 寄生式继承

function object(obj){
  function Func(){}
  Func.prototype = obj
  return new Func
}

/**
 *  createAnother()函数接收一个参数,就是新对象的基准对象,这个对象origin会
 *  被传给object()函数,然后将返回的新对象赋值给clone, 接着clone对象添加一个新方法
 *  sayHi()。最后返回这个新对象
 */
function createAnother(origin){
  // 通过调用函数创建一个新对象
  let clone = object(origin)
  // 增强新对象的方法
  clone.sayHi = function(){
    console.log('Hi...');
  }
  return clone
}

let person = {
  name: 'Nicholas',
  friends: ['Shilby','Bob']
}

let person1 = createAnother(person)

person1.sayHi() // Hi...

  1. 寄生式继承同样适合主要关注对象,以上例子中object()函数不是寄生式继承所必须的,任何返回新对象的函数都可以代替object()函数。

1.3.6 寄生式组合继承

  1. 组合继承其实也存在效率问题。最主要的效率问题就是父类构造函数始终会被调用两次: 一次在创建子类原型时调用,另一次是在子类构造函数中调用。本质上子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。

// 组合继承

function SuperType(name){
  this.name = name
  this.colors = ['red','green','blue']
}
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(){
  this.age = age
}

/**
 *  上面代码执行后,SubType.prototype上会有两个属性:name和colors。它们都是SuperType的实例属性,
 *  但现在成为了SubType的原型属性。在调用SubType构造函数时,也会调用SuperType构造函数,这一次会在
 *  新对象上创建实例属性name和colors 这两个实例属性会遮蔽原型上的同名属性。
 */

image.png 2. 如上图,有两组name和colors属性:一组在实例上,另一组在SubType的原型上。这是调用两次SuperType构造函数的结果。 3. 寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。说到底就是寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。


// 寄生式组合继承

/**
 *  基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。
 *  (就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型)
 */

function object(obj){
  function Func(){}
  Func.prototype = obj
  return new Func()
}

// 实现寄生式组合继承的核心函数
function inheritPrototype(SubType,SuperType){
  let clone = object(SuperType.prototype) // 创建对象
  clone.constructor = SubType // 增强对象
  SubType.prototype = clone // 赋值对象
}

// 父类构造函数
function SuperType(name){
  this.name = name,
  this.colors = ['red','olivedrab']
}
// 父类构造函数原型方法
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);
}

let person1 = new SubType('Nicholas',22)
person1.sayName() // Nicholas
person1.sayAge()  // 22
console.log(person1.colors); // [ 'red', 'olivedrab' ]
console.log(person1 instanceof SuperType); // true

/**
 *  1.这个inheritPrototype()函数实现了寄生式组合继承的核心原理。这个函数接收两个参数:子类构造函数和父类构造函数。
 *  在这个函数的内部,第一步是创建父类原型的一个副本。然后,给返回的clone对象设置constructor属性,解决由于重写
 *  原型导致默认constructor丢失的问题。最后将新对象赋值给子类构造函数的原型。
 *  2.这种寄生式组合继承方式,SuperType父类构造函数只调用了一次,避免了子类构造函数原型(SubType.prototype)上用不到的属性
 *  因此可以说这样的方式更加的高效。而且原型链仍然保持不变。
 *  3.寄生式组合继承可以算是引用类型继承的最佳方式。
 */

1.4 类

  1. ECMAScript6新引入的class关键字具有正式定义类的能力。实际上背后使用的仍然是原型和构造函数的概念。

1.4.1 类定义

  1. 与函数类型相似,定义类也有两种主要方式:类声明和类表达式。
  2. 与函数表达式类似,类表达式在它们被求值前不能引用。不过,与函数定义不同的是,虽然函数声明可以提升,但类定义不能。
  3. 另一个跟函数声明不同的地方是,函数受函数作用域限制,而类受作用域限制。

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

console.log(FunctionExpression); // undefined
var FunctionExpression = function(){}
console.log(FunctionExpression); // [Function: FunctionExpression]

console.log(FunctionDeclaration); // [Function: FunctionDeclaration]
function FunctionDeclaration(){}
console.log(FunctionDeclaration); // [Function: FunctionDeclaration]

console.log(ClassExpression); // undefined
var ClassExpression = class {}
console.log(ClassExpression); // [class ClassExpression]

console.log(ClassDeclaration); // Error
class ClassDeclaration {}
console.log(ClassDeclaration); // [class ClassDeclaration]

// 跟函数声明不同的是,函数受函数作用域的限制,而类受块作用域的限制
{
  function FuncDeclaration() {}
  class ClassDeclaration2 {}
}
console.log(FuncDeclaration); // [Function: FuncDeclaration]
console.log(ClassDeclaration2); // Error
  1. 类的构成
    1. 类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法,但这些都不是必须的,空的类定义照样有效。
    2. 类名大写,以区别通过它创建的实例
    3. 类表达式的名称是可选的。在把类表达式赋值给变量后,可以通过name属性取得类表达式的名称字符串。
// 空类定义,有效
class Person {}

// 有构造函数的类,有效
class Bar {
  constructor(){}
}

// 获取函数的类,有效
class Baz {
  get myBaz(){}
}

// 有静态方法的类,有效
class Qux {
  static myQux(){}
}

let Person1 = class PersonName{
  identify(){
    console.log(Person1.name,PersonName.name);
  }
}

let p = new Person1()
p.identify() // PersonName PersonName

1.4.2 类构造函数

  1. constructor关键字用于在类定义块内部创建类的构造函数。方法名constructor会告诉解释器在使用new操作符创建类的新实例时,应该调用这个函数。构造函数不是必须的,不定义构造函数相当于将构造函数定义为空函数。
  2. 实例化。使用new操作符实例化Person的操作等于使用new调用其构造函数。Javascript解释器知道使用new和类意味着应该使用constructor函数进行实例化。使用new 调用类的构造函数会执行如下操作。
    1. 在内存中创建一个新对象
    2. 这个新对象内部的prototype 指针被赋值为构造函数的prototype属性
    3. 构造函数内部的this被赋值为这个新对象
    4. 执行构造函数内部的代码
    5. 如果构造函数返回非空对象,则返回该对象;否则返回刚创建的新对象。
  3. 类实例化时传入的参数会用作构造函数的参数。如果不需要参数,则类名后面的括号也是可选的。
class Animal {}

class Person {
  constructor() {
    console.log("person ctor");
  }
}

class Vegetable {
  constructor() {
    this.color = "olivedrab";
  }
}

let a = new Animal();
let p = new Person();
let v = new Vegetable();
console.log(v.color); // olivedrab

// 类实例化时传入的参数会作为构造函数的参数。如果不需要参数,则类名后面的括号也是可选的
class Person1 {
  constructor(name){
    console.log(arguments.length);
    this.name = name || null
  }
}

let p1 = new Person1; // 0
console.log(p1.name); // null
let p2 = new Person1('Nicholas') // 1
console.log(p2.name); // Nicholas
  1. 默认情况下,类构造函数会在执行之后返回this对象。构造函数返回的对象会被用作实例化对象。如果没有什么引用新创建的this对象,那么这个对象会被销毁。不过,如果返回的不是this对象,而是其他对象,那么这个对象不会通过instanceof操作符检测出跟类有关联,因为这个对象的原型指针并没有被修改。
class Person {
  constructor(override){
    this.override = override
    if(override){
      return {
        'foo': 'baz'
      }
    }
  }
}

let p1 = new Person()
console.log(p1 instanceof Person); // true
let p2 = new Person('ready')
console.log(p2 instanceof Person); // false
  1. 类构造函数与构造函数的主要区别是,调用类构造函数必须使用new操作符。而普通构造函数如果不适用new调用,那么就会以全局的this作为内部对象。调用类构造函数时如果网络使用new则会抛出错误。
function Person(){}
class Animal {}

let p = Person()
let a = new Animal()

// 类构造函数没有什么特殊之处,实例化之后,它会成为普通的实例方法。因此,实例化之后可以在实例上引用它
let a2 = new Animal()

let a3 = new a2.constructor()
  1. 把类当成特殊函数
    1. 声明一个类后,通过typeof操作符检测类标识符,表明它是一个函数。
    2. 类标识符有prototype属性,而这个原型也有一个constructor属性指回类自身
    3. 与普通构造函数一样,可以使用instanceof操作符检查构造函数原型是否存在于实例的原型链中;
    4. 类是JavaScript的一等公民,因此可以像其他对象或函数引用一样把类作为参数传递
class Person {}
console.log(typeof Person); // function
console.log(Person.prototype.constructor == Person); // true

let p = new Person()
console.log(p instanceof Person); // 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],21) // instance 21

1.4.3 实例、原型和类成员

  1. 实例成员 每次通过new调用类标识符时,都会执行类构造函数。在这个构造函数内部,可以为新创建的实例添加自有属性。另外在构造函数执行完毕后,仍然可以给实例继续添加新成员。
class Person {
  constructor(){
    this.name = new String('Jack')
    this.sayName = () => {
      console.log(this.name);
    }
    this.nickName = ['Jack','J-dog']
  }
}
let person1 = new Person()
let person2 = new Person()

console.log(person1.name == person2.name); // false
person1.name = person1.nickName[0]
person2.name = person2.nickName[1]

person1.sayName() // Jack
person2.sayName() // J-dog
  1. 原型方法与访问器
    1. 为了在实例间共享方法,类定义语法把类块中定义的方法作为原型方法。
    2. 可以把方法定义在类构造函数中或者类块中,但不能在类块中给原型添加原始值或对象作为成员数据
    3. 类定义也支持获取和设置访问器。语法跟普通对象一样

// 类方法等同于对象属性,因此可以使用字符串、符号或计算的值作为键
const symbolKey = Symbol('symbolKey')

class Person {
  constructor(){}
  stringKey(){
    console.log('invoked stringKey');
  }
  [symbolKey](){
    console.log('invoked symbolKey');
  }
  ['computed'+'key'](){
    console.log('invoked computed-key');
  }
}
let p = new Person()
p.stringKey() // invoked stringKey
p[symbolKey]() // invoked symbolKey
p.computedkey() // invoked computed-key

// 获取和设置访问器
class Person1 {
  constructor(){}
  set name(newName){
    this.name_ = newName
  }
  get name(){
    return this.name_
  }
}
let p2 = new Person1()
p2.name = 'Jack'
console.log(p2.name); // Jack
  1. 静态类方法 静态成员每个类上只能有一个

class Person {
  constructor(){
    this.locate = function(){
      console.log('instance',this);
    }
  }
  // 定义在类的原型对象上
  locate(){
    console.log('prototype',this);
  }
  // 定义在类本身上
  static locate(){
    console.log('class',this);
  }
}
let p = new Person()
p.locate() // instance Person { locate: [Function (anonymous)] }
Person.prototype.locate() // prototype {}
Person.locate() // class [class Person]

1.4.4 继承

  1. ES6类支持单继承。使用extends关键字,就可以继承任何拥有[[Constructor]]和原型的对象。
class Vehicle {}
// 继承类
class Bus extends Vehicle {}

var b = new Bus()
console.log(b instanceof Bus); // true
console.log(b instanceof Vehicle); // true

class Person {}
// 继承普通的构造函数
class Engineer extends Person{}

var e = new Engineer()
console.log(e instanceof Engineer); // true
console.log(e instanceof Person); // true

// 派生类都会通过原型链访问到类和原型上定义的方法。this的值反映调用相应方法的实例或类
class Vehicle1 {
  identifyPrototype(id){
    console.log(id,this);
  }
  static identifyPrototype(id){
    console.log(id,this);
  }
}
class Bus1 extends Vehicle1 {}

var v = new Vehicle1()
var b2 = new Bus1()

v.identifyPrototype('vehicle1') // vehicle1 Vehicle1 {}
b2.identifyPrototype('bus') // bus Bus1 {}
  1. 构造函数、HomeObject和super()
    1. 派生类的方法可以通过super关键字引用它们的原型。这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。在类构造函数中使用super可以调用父类构造函数。
    2. 在静态方法中可以通过super调用继承的类上定义的静态方法
class Vehicle {
  constructor(){
    this.hasEngine = true
  }
}
class Bus extends Vehicle {
  constructor(){
    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
  1. 在使用super时要注意的问题
    1. super只能在派生类构造函数和静态方法中使用
    2. 不能单独引用super关键字,要么用它调用构造函数,要么用它调用静态方法
    3. 调用super()回调用父类构造函数,并将返回的实例赋值给this
    4. super()的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入
    5. 如果没有定义类构造函数,在实例化派生类时会调用super(),而且会传入所有传给派生类的参数
    6. 在类构造函数中,不能在调用super()之间引用this
    7. 如果在派生类中显式定义了构造函数,则要么必须在其中调用super(),要么必须在其中返回一个对象

// 1. super只能在派生类构造函数和静态方法中使用
// class Vehicle {}
// 2. 不能单独引用super关键字,要么用它调用构造函数,要么用它引用静态方法
class Bus extends Vehicle{
  constructor(){
    console.log(super); // SyntaxError
  }
}
// 3. 调用super()会调用父类构造函数,并将返回的实例赋值给this
class Bus extends Vehicle {
  constructor(){
    super()
    console.log(this instanceof Vehicle); // true
  }
}
new Bus()
// 4. super()的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入
class Vehicle {
  constructor(plate){
    this.plate = plate
  }
}

class Bus extends Vehicle {
  constructor(plate){
    super(plate)
  }
}

var b =  new Bus('13337HX')
console.log(b); // Bus { plate: '13337HX' }
// 5. 如果没有定义类构造函数,在实例化派生类时会调用super()而且会 传入所有传给派生类的参数
class Vehicle {
  constructor(plate){
    this.plate = plate
  }
}
class Bus extends Vehicle {}
var b = new Bus('AE86')
console.log(b); // Bus { plate: 'AE86' }

// 6. 如果在派生类中显示的定义了构造函数,则要么必须在其中调用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); // {}

  1. 抽象基类
    1. 定义一个类,它可供其他类继承,但本身不会被实例化。通过new.target实现。
    2. 通过在实例化时检测new.target是不是抽象类,可以阻止对抽象基类的实例化
class Vehicle {
  constructor(){
    console.log(new.target);
    if(new.target == Vehicle){
      throw new Error('error')
    }
  }
}
// 派生类
class Bus extends Vehicle {}
new Bus()
new Vehicle()