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

对象、类与面向对象编程


1. 理解对象

ECMA-262将对象定义为一组属性的无序集合,可以将对象看成一张散列表,其中的内容就是一组名/值对,值可以是数据或者函数

  • 创建 Object 的一个新实例,然后再给它添加属性和方法
  • 使用对象字面量创建对象
// 创建 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); 
 } 
};

1. 属性的类型

为了将某个特性表示为内部特性,规范使用两个中括号把特性名称括起来,比如[[Enumerable]]

1. 数据属性

数据属性包含一个保存数据值的位置,值可以从该位置被读取写入,有相应的4个特性:

  • [[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改特性以及改为访问器属性,默认为true
  • [[Enumerable]]:表示属性是否可以通过for-in循环返回,默认为true
  • [[Writable]]:表示属性的值是否可以被修改,默认为true
  • [[Value]]:包含属性实际的值,默认为undefined

将属性显式地添加到对象后,前三个特性都会被设置为true,而最后一个被设置为特定的值

要修改属性的默认特性,则必须使用Object.defineProperty()(可参考杂乱知识点),该方法接收三个参数:添加属性的对象、属性名称以及描述符对象。描述符对象中属性可以包含configurableenumerablewritablevalue

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"。这个属性的值就不能再修改了,在非严格模式下尝试给这个属性重新赋值会被忽略。在严格模式下,尝试修改只读属性的值会抛出错误。

一个属性被定义为不可配置后,就不能再变回被配置的了,再次调用Object.defineProperty()并修改任何非writable属性都会导致报错,即只有一开始定义为可写的属性,在之后才能修改

let person = {}
Object.defineProperty(person, "name" ,{
  configurable: false,
  value: "Nicholas"
})
// Uncaught TypeError: Cannot redefine property: name
Object.defineProperty(person, "name", {
  configurable: true,
  value: "Nicholas"
})

// 不报错
let person = {}
Object.defineProperty(person, "name" ,{
  configurable: false,
  writable: true,
  value: "Nicholas"
})
// Uncaught TypeError: Cannot redefine property: name
Object.defineProperty(person, "name", {
  writable: true,
  value: "Abc"
})

调用 Object.defineProperty()时,configurable、enumerable 和 writable 的值如果不指定,则都默认为 false

2. 访问器属性

访问器属性不包含数据值,而可选地包含一个获取函数和一个设置函数。且访问器属性包含以下4个特性:

  • [[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改特性以及改为访问器属性,默认为true
  • [[Enumerable]]:表示属性是否可以通过for-in循环返回,默认为true
  • [[Get]]:获取函数,读取属性时调用,默认undefined
  • [[Set]]:设置函数,写入属性时调用,默认undefined

访问器属性只能通过Object.defineProperty()来定义

// 定义一个对象,包含伪私有成员 year_和公共成员 edition
let book = {
  year_: 2017,
  edition: 4
}
Object.defineProperty(book, "year", {
  get(){
    return this.year_
  },
  set(newVal){
    if(newVal > 2017){
      this.year_ = newVal
      this.edition += newVal - 2017
    }
  }
})
book.year = 2018
console.log(book)
console.log(book.year_ == book.year)  // 在Object.defineProperty中需要加上this
console.log(book.year_)
console.log(book.year)
console.log(book.edition)

获取函数和设置函数不一定都要定义。只定义获取函数意味着属性是只读的,尝试修改属性会被忽略。在严格模式下,尝试写入只定义了获取函数的属性会抛出错误。类似地,只有一个设置函数的属性是不能读取的,非严格模式下读取会返回 undefined,严格模式下会抛出错误


2. 定义多个属性

可以使用Object.defineProperties()方法一次性定义多个属性,接收两个参数:要添加或修改属性的对象以及另外一个描述符对象

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

唯一的区别是所有属性都是同时定义的,并且数据属性的configurableenumerablewritable特性值都是 false,即因为没有定义,因此此时的默认值都为false。若直接在对象上添加属性,则默认都为true


3. 读取属性的特性

可以使用Object.getOwnPropertyDescriptor()方法取得指定属性的属性描述符,接收两个参数:属性所在对象以及要取得的描述符属性名。返回值是一个对象

// 此处使用的例子来源于上一小节
let descriptor = Object.getOwnPropertyDescriptor(book, "year_") // 数据属性
console.log(descriptor.value) // 2017
console.log(descriptor.configurable)  // false
console.log(typeof descriptor.get)  // "undefined"

let descriptor1 = Object.getOwnPropertyDescriptor(book, "year") // 访问器属性
console.log(descriptor1.value)  // "undefined"  因为实际上访问器属性没有值,只是通过get方法获取数据属性的值
console.log(descriptor1.enumerable) // false
console.log(typeof descriptor1.get) // "function"	其为指向获取函数的指针

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

// 此处使用的例子来源于上一小节
console.log(Object.getOwnPropertyDescriptors(book))
// {year_: {…}, edition: {…}, year: {…}}
//  edition:{value: 1, writable: false, enumerable: false, configurable: false}
//    configurable:false
//    enumerable:false
//    value:1
//    writable:false
//    [[Prototype]]:Object
//  year:{get: ƒ, set: ƒ, enumerable: false, configurable: false}
//    configurable:false
//    enumerable:false
//    get:ƒ get(){\r\n      return this.year_\r\n    }
//    set:ƒ set(newVal){\r\n      if(2017 < newVal){\r\n        this.year_ = newVal\r\n        this.edition += newVal - this.year_\r\n      }\r\n    }
//    [[Prototype]]:Object
//  year_:{value: 2017, writable: false, enumerable: false, configurable: false}
//    configurable:false
//    enumerable:false
//    value:2017
//    writable:false

//简单点来说,其实就是以下输出
// { 
// edition: { 
// configurable: false, 
// enumerable: false, 
// value: 1, 
// writable: false 
// }, 
// year: { 
// configurable: false, 
// enumerable: false, 
// get: f(), 
// set: f(newValue), 
// }, 
// year_: { 
// configurable: false, 
// enumerable: false, 
// value: 2017, 
// writable: false 
// } 
// }

4. 合并对象

合并–将源对象所有的本地属性一起复制到目标对象上,该操作也可称为“混入(mixin)”,因为目标对象通过混入源对象属性得到增强

Object.assign()用于合并对象,接收一个目标对象以及一或多个源对象为参数,将每个源对象中的可枚举(Object.propertyIsEnumerable()返回true)和自有(Object.hasOwnProperty()返回true)属性复制到目标对象

字符串以及符号为键的属性会被复制,使用源对象上的[[Get]]取得属性值,使用目标对象上的[[Set]]设置属性值

let desc, src, result
// 简单复制
desc = {}
src = { id: 'src' }
// 第一个参数为目标对象,后面参数全为源对象,调用该方法会返回修改后的目标对象
result = Object.assign(desc, src)
console.log(desc === result)  // true
console.log(desc !== src)     // true  因为对象地址不同,但其中属性地址相同
console.log(desc.id === src.id)   // true
console.log(result)   // {id: 'src'}
console.log(desc)     // {id: 'src'}

// 多个源对象
desc = {}
result = Object.assign(desc, { a: 'foo' }, { b: 'bar' })
console.log(result) // {a: 'foo', b: 'bar'}

// 获取函数与设置函数
desc = {
  set a(val){
    // 设置a这个属性的设置函数,由于该函数内部并未进行修改或赋值,因此没有转移值
    console.log(`Invoked desc setter with params ${val}`)
  }
}
src = {
  get a(){
    console.log('Invoke src getter')
    return 'foo'
  }
}
Object.assign(desc, src)
// 调用 src 的获取方法
// 调用 dest 的设置方法并传入参数"foo" 
// 因为这里的设置函数不执行赋值操作
// 所以实际上并没有把值转移过来
console.log(desc) // { set a(val) {...} }

Object.assign()实际上对每个对象执行的是浅复制,若多个源对象拥有相同属性,则使用最后一个复制的值

从源对象访问器属性中取得的值,会作为一个静态值赋给目标对象,即不能再两个对象间转移获取函数和设置函数

let dest, src, result
// 覆盖属性
dest = {id:'dest'}
result = Object.assign(dest, {id:'src1',a:'foo'},{id:'src2',b:'bar'})
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)
console.log(dest.a === src.a) // true

若赋值期间出错,会终止并退出,抛出错误;Object.assign()没有回滚,因此目标对象上已经完成的修改会继续存在

let dest, src, result; 
/** 
 * 错误处理
 */ 
dest = {}; 
src = { 
 a: 'foo', 
 get b() { 
 // Object.assign()在调用这个获取函数时会抛出错误
 throw new Error(); 
 },
 c: 'bar' 
}; 
try { 
 Object.assign(dest, src); 
} catch(e) {} 
// Object.assign()没办法回滚已经完成的修改
// 因此在抛出错误之前,目标对象上已经完成的修改会继续存在:
console.log(dest); // { a: foo }

5. 对象标识及相等判定
//  ECMAScript 6 之前,有些特殊情况即使是===操作符也无能为力
// 这些是===符合预期的情况
console.log(true === 1); // false 
console.log({} === {}); // false 
console.log("2" === 2); // false 
// 这些情况在不同 JavaScript 引擎中表现不同,但仍被认为相等
// 因为使用的是SameValue,不能判定+0与-0的关系,都判等
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
// 为改善这类情况,ECMAScript 6 规范新增了 Object.is(),这个方法与===很像,但同时也考虑到了上述边界情形。这个方法必须接收两个参数
console.log(Object.is(true, 1)); // false 
console.log(Object.is({}, {})); // false 
console.log(Object.is("2", 2)); // false 
// 正确的 0、-0、+0 相等/不等判定
// 此处使用的是SameValueZero
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
// 要检查超过两个值,递归地利用相等性传递即可
function recursiveCheckEqual(x, ...rest) {
  return Object.is(x, rest[0])&&(rest.length > 2&&recursiveCheckEqual(...rest))
}
console.log(recursiveCheckEqual(1,24,5,624,2,5))

6. 增强的对象语法
1. 属性值简写

给对象添加变量的时候,开发者经常会发现属性名和变量名是一样的,因此,简写属性名语法出现了。简写属性名只要使用变量名(不用再写冒号)就会自动被解释为同名的属性键。如果没有找到同名变量,则会抛出 ReferenceError

let name = 'Matt'; 
let person = { 
 name 			// 简写
}; 
console.log(person); // { name: 'Matt' }
2. 可计算属性

在引入可计算属性之前,如果想使用变量的值作为属性,那么必须先声明对象,然后使用中括号语法来添加属性。换句话说,不能在对象字面量中直接动态命名属性

有了可计算属性,就可以在对象字面量中完成动态属性赋值。中括号包围的对象属性键告诉运行时将其作为 JavaScript 表达式而不是字符串来求值

// 原情况应使用
const nameKey = "name"
const ageKey = "age"
const jobKey = "job"

let person = {}
person[nameKey] = "Matt"
person[ageKey] = 27
person[jobKey] = "SoftWare engineer"
console.log(person)// {name: 'Matt', age: 27, job: 'SoftWare engineer'}
// 引入后情况
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'}

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

const nameKey = 'name'; 
const ageKey = 'age'; 
const jobKey = 'job'; 
let uniqueToken = 0; 
function getUniqueKey(key) { 
 return `${key}_${uniqueToken++}`; 
} 
let person = { 
 [getUniqueKey(nameKey)]: 'Matt', 
 [getUniqueKey(ageKey)]: 27, 
 [getUniqueKey(jobKey)]: 'Software engineer' 
}; 
console.log(person); // { name_0: 'Matt', age_1: 27, job_2: 'Software engineer' }

可计算属性表达式中抛出任何错误都会中断对象创建,且不能回滚

3. 简写方法名
let person = { 
 sayName(name) { // 简写
 console.log(`My name is ${name}`); 
 } 
}

// 简写方法名对获取函数和设置函数也是适用的
let person = { 
 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}`); 
 } 
} 
person.sayName('Matt'); // My name is Matt

7. 对象解构

对象解构语法,可以在一条语句中使用嵌套数据实现一个或多个赋值操作。对象解构就是使用与对象匹配的结构来实现对象属性赋值

// 不使用对象解构
let person = {
  name: 'Matt',
  age: 27
}
let personName = person.name,
personAge = person.age
console.log(personName) // 'Matt'
console.log(personAge)  // 27

// 使用对象解构
let person1 = {
  name: 'Matt',
  age: 27
}
let { name: personName1, age: personAge1 } = person1 // 此处的name以及age都是person1内的属性
console.log(personName1)  // 'Matt'
console.log(personAge1)   // 27

// 若是变量直接使用属性的名称,可以直接简写语法
let person = {
  name: 'Matt',
  age: 27
}
let { name, age } = person
console.log(name) // 'Matt'
console.log(age)  // 27

// 解构赋值不一定与对象属性匹配
// 赋值时可以忽略某些属性,若引用属性不存在,则该变量的值就是undefined
let person = {
  name: 'Matt',
  age: 27
}
let { name, job } = person	// job并未在person内被定义过
console.log(name) // 'Matt'
console.log(job)  // undefined

// 可以定义默认值,在引用属性不存在时即可使用
let person = {
  name: 'Matt',
  age: 27
}
let { name, job='Software engineer' } = person
console.log(name) // 'Matt'
console.log(job)  // 'Software engineer'

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

let {length} = 'foobar'// 被转化为String对象,此时其存在length属性即为其长度
console.log(length)// 6

let {constructor: c} = 4//将其转化为Number对象,其构造器也为Number
console.log(c === Number)// true

let {_} = null //抛出错误
let {_} = undefined //抛出错误

// 补充
let { constructor: c } = 4;
console.log(constructor)  // ƒ Window()
console.log(c)  // ƒ Number()

let { constructor } = 4
console.log(constructor)  // ƒ Number()

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

let personName, personAge
let person = {
  name: 'Matt',
  age: 27
}
({name: personName, age: personAge} = person)
console.log(personName, personAge)
1. 嵌套解构
// 通过解构来复制对象属性
let person = {
  name: "Matt",
  age: 27,
  job: {
    title: 'Software engineer'
  }
}
let personCopy = {}
({name: personCopy.name, 
  age: personCopy.age,
  job: personCopy.job
} = person)
console.log(personCopy)// {name: 'Matt', age: 27, job: {…}}

// 因为一个对象的引用被赋值给 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
3. 参数上下文匹配

在函数参数列表中也可以进行解构赋值。对参数的解构赋值不会影响 arguments 对象,但可以在函数签名中声明在函数体内使用局部变量:

let person = { 
 name: 'Matt', 
 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: 'Matt', age: 27 }, '2nd'] 
// 'Matt', 27 
printPerson2('1st', person, '2nd'); 
// ['1st', { name: 'Matt', age: 27 }, '2nd'] 
// 'Matt', 27

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")
console.log(person1)// {name: 'Nicholas', age: 29, job: 'Software Engineer', sayName: ƒ}
console.log(person2)// {name: 'Greg', age: 27, job: 'Doctor', sayName: ƒ}

这种工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)


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")
console.log(person1)//Person {name: 'Nicholas', age: 29, job: 'Software Engineer', sayName: ƒ}
console.log(person2)//Person {name: 'Greg', age: 27, job: 'Doctor', sayName: ƒ}
person1.sayName()//Nicholas
person2.sayName()//Greg

构造函数模式与工厂模式的区别:

  • 没有显示地创建对象
  • 属性和方法直接赋给了this
  • 没有return

构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头

使用构造函数创建实例,应该使用new操作符

  • 内存中创建一个新对象(没有显示创建)
  • 新对象内部地[[Prototype]]特性被赋值为构造函数的prototype属性
  • 构造函数内部的this被赋值为这个新对象(即**this指向新对象**)
  • 执行构造函数内部的代码(给新对象添加属性)
  • 若构造函数返回非空对象,则返回该对象;否则返回刚创建的对象

constructor本身是用于标识对象类型的,一般认为instanceof更为可靠

定义自定义构造函数可以确保实例被标识为特定类型,所有自定义对象都继承自 Object

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

console.log(person1 instanceof Person)// true
console.log(person1 instanceof Object)// true
console.log(person2 instanceof Person)// true
console.log(person2 instanceof Object)// true

构造函数不一定要写成函数声明的形式,赋值给变量的函数表达式也可以表示构造函数。在实例化时,如果不想传参数,那么构造函数后面的括号可加可不加。只要有 new 操作符,就可以调用相应的构造函数

let Person = function(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 instanceof Object); // true 
console.log(person1 instanceof Person); // true 
console.log(person2 instanceof Object); // true 
console.log(person2 instanceof Person); // true
// 不传参
function Person() { 
 this.name = "Jake"; 
 this.sayName = function() { 
 console.log(this.name); 
 }; 
} 
let person1 = new Person(); 
let person2 = new Person;
1.构造函数也是函数

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

// 作为构造函数
let person = new Person("Matt", 27, "Doctor")
person.sayName()  // "Matt"

// 作为函数调用
Person("Greg", 27, "Teacher") // 添加到window对象
window.sayName()  // "Greg"

// 在另一个对像作用域中调用
let o = new Object()
Person.call(o, "Tom", 25, "Nurse")
o.sayName()   // "Tom"
// 这里的调用将对象 o 指定为 Person()内部的 this 值,因此执行完函数代码后,所有属性和 sayName()方法都会添加到对象 o 上面。

在调用一个函数而没有明确设置 this的情况下(即没有作为对象的方法调用,或者没有使用 call()/apply()调用),this始终指向 Global对象(在浏览器中就是 window 对象)

2. 构造函数的问题

构造函数的主要问题在于,其定义的方法会在每个实例上都创建一遍。person1 和 person2 都有名为 sayName()的方法,但这两个方法不是同一个 Function 实例,不同实例上的函数虽然同名却不相等

做一样的事,所以没必要定义两个不同的 Function 实例。况且,this 对象可以把函数与对象的绑定推迟到运行时

// 原方法
function Person(name, age, job){ 
 this.name = name; 
 this.age = age; 
 this.job = job; 
 this.sayName = new Function("console.log(this.name)"); // 逻辑等价
}

// 函数定义转移到构造函数外部
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

这一次 sayName 属性中包含的只是一个指向外部函数的指针,所以 person1 和 person2共享了定义在全局作用域上的 sayName()函数

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


3. 原型模式

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

原型对象上定义的属性和方法可以被对象实例共享,原来在构造函数中直接赋给对象实例的值,可以直接赋予其原型

function Person() {}
Person.prototype.name = "Nicholas"
Person.prototype.age = 27
Person.prototype.job = "Doctor"
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 People = function(){}
People.prototype.name = "Matt"
People.prototype.age = 28
People.prototype.job = "Nurse"
People.prototype.sayName = function(){
  console.log(this.name)
}
let person11 = new People()
person11.sayName()// Matt
let person22 = new People
person22.sayName()// Matt
console.log(person11.sayName === person22.sayName)// true
1. 理解原型

参考地址

  • 对象实例的__proto__指向构造函数的原型prototype
  • prototype.constructor是prototype上的一个保留属性,这个属性就指向类函数本身,用于指示当前类的构造函数 ,因构造函数而异,可能会给原型对象添加其他属性和方法
  • 只要创建一个函数,就会按照特定规则为该函数创建一个prototype属性,指向原型对象
  • 自定义构造函数时,原型对象默认只会获得constructor属性,其它所有方法都继承自Object,创建实例时,[[Prototype]]指针即赋值为构造函数的原型对象
/** 
 * 构造函数可以是函数表达式
 * 也可以是函数声明,因此以下两种形式都可以:
 * function Person() {} 
 * let Person = function() {} 
 */
function Person() {}
/** 
 * 声明之后,构造函数就有了一个
 * 与之关联的原型对象:
 */
console.log(typeof Person.prototype)// object
console.log(Person.prototype)
/**
 * {constructor: ƒ}
 *    constructor:ƒ 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()
let 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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YOwfdqrO-1641778705788)(assert/8%E5%AF%B9%E8%B1%A1%E3%80%81%E7%B1%BB%E4%B8%8E%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%BC%96%E7%A8%8B.assert/1641707531401.png)]

原型对象包含 constructor 属性和其他后来添加的属性,实例与构造函数没有直接关系。虽然两个实例都没有属性和方法,但可以正常使用以及调用,是因为对象属性查找机制的原因

虽然不是所有实现都对外暴露了[[Prototype]],但可以使用 isPrototypeOf()方法确定两个对象之间的这种关系,isPrototypeOf()会在传入参数的[[Prototype]]指向调用它的对象时返回 true

function Person(){}
let person1 = new Person
let person2 = new Person
console.log(Person.prototype.isPrototypeOf(person1))// true 即Person.prototype是person1的[[Prototype]]指向

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

function Person(){}
let person1 = new Person
let person2 = new Person
console.log(Object.getPrototypeOf(person1) === Person.prototype)// true
console.log(Object.getPrototypeOf(person1).name)// undefined

Object 类型还有一个 setPrototypeOf()方法,可以向实例的私有特性[[Prototype]]写入一个新值。这样就可以重写一个对象的原型继承关系

function Person(){}
let biped = {
  numLegs: 2
}
let person = {
  name: 'Matt'
}
Object.setPrototypeOf(person, biped)
console.log(person.name)// Matt
console.log(person.numLegs)// 2
console.log(person.__proto__ === biped)// true
// 原来的属性还可以调用,传入的新值也可以使用
console.log(person)
/**
 * {name: 'Matt'}
 *    name:'Matt'
 *    [[Prototype]]:Object
 *      numLegs:2
 *      [[Prototype]]:Object
 *        constructor:ƒ Object()
 */
console.log(person.__proto__)
// {numLegs: 2}

为避免使用 Object.setPrototypeOf()可能造成的性能下降,可以通过 Object.create()来创建一个新对象,同时为其指定原型

function Person(){}
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. 原型层级

在通过对象访问属性时,会按照这个属性的名称开始搜索。搜索开始于对象实例本身。如果在这个实例上发现了给定的名称,则返回该名称对应的值。如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值。

constructor属性只存在于原型对象

只要给对象实例添加一个属性,这个属性就会遮蔽(shadow)原型对象上的同名属性,也就是虽然不会修改它,但会屏蔽对它的访问

使用 delete操作符可以完全删除实例上的这个属性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JUE1lpPF-1641778705789)(assert/8%E5%AF%B9%E8%B1%A1%E3%80%81%E7%B1%BB%E4%B8%8E%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%BC%96%E7%A8%8B.assert/1641709765335.png)]

hasOwnProperty()方法用于确定某个属性是在实例上还是在原型对象上。这个方法是继承自 Object的,会在属性存在于调用它的对象实例上时返回 true

ECMAScript 的 Object.getOwnPropertyDescriptor()方法只对实例属性有效。要取得原型属性的描述符,就必须直接在原型对象上调用该方法

function Person() {}
Person.prototype.name = "Matt"
Person.prototype.age = 27
Person.prototype.job = "Doctor"
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)// "Matt"
console.log(person2.hasOwnProperty("name"))// false

delete person1.name
console.log(person1.name)// "Matt"
console.log(person1.hasOwnProperty("name"))// false
3. 原型和in操作符

两种方式使用in单独使用和在for-in循环中利用

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

    console.log("name" in person1)
    

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

    // hasOwnProperty()用于确定是否存在于实例上
    function hasPrototypeProperty(object, name){
        return !Object.hasOwnProperty(name) && (name in object)
        // 即不在实例上但能通过in访问到
        // 若在实例上遮蔽了原型上的属性,通过该方法获得false
    }
    
  • for-in循环中使用 in操作符时,可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性。遮蔽原型中不可枚举([[Enumerable]]特性被设置为 false)属性的实例属性也会在 for-in 循环中返回,因为默认情况下开发者定义的属性都是可枚举的

    获得对象上所有可枚举的实例属性,可以使用 Object.keys()方法。这个方法接收一个对象作为参数,返回包含该对象所有可枚举属性名称的字符串数组,不包含原型上的

    function Person() {} 
    Person.prototype.name = "Nicholas"
    Person.prototype.age = 29
    Person.prototype.job = "Software Engineer"
    Person.prototype.sayName = function() { 
     console.log(this.name)
    }
    let keys = Object.keys(Person.prototype)
    console.log(keys)// "name,age,job,sayName" 
    let p1 = new Person()
    let p1Keys = Object.keys(p1)
    console.log(p1Keys)// (0) [] 即keys只返回该对象上的属性,不返回原型上的
    p1.name = "Rob"
    p1.age = 31
    let p1keys = Object.keys(p1)
    console.log(p1keys)// "[name,age]"
    
  • 想列出所有实例属性,无论是否可以枚举,都可以使用 Object.getOwnPropertyNames()

let keys = Object.getOwnPropertyNames(Person.prototype); 
console.log(keys); // "[constructor,name,age,job,sayName]"
  • 在 ECMAScript 6 新增符号类型之后,相应地出现了增加一个 Object.getOwnPropertyNames()的兄弟方法的需求,因为以符号为键的属性没有名称的概念。因此,Object.getOwnPropertySymbols()方法就出现了,这个方法与 Object.getOwnPropertyNames()类似,只是针对符号而已
let k1 = Symbol('k1'), 
 k2 = Symbol('k2');
let o = { 
 [k1]: 'k1', 
 [k2]: 'k2' 
}; 
console.log(Object.getOwnPropertySymbols(o)); 
// [Symbol(k1), Symbol(k2)]
4. 属性枚举顺序

for-in循环与Object.keys()的枚举顺序不确定

Object.getOwnPropertyNames()Object.getOwnPropertySymbols()以及Object.assign()的枚举顺序如下:首先以升序枚举数值键,之后以插入顺序(即在声明时的顺序)枚举字符串和符号键

let k1 = Symbol('k1'), 
 k2 = Symbol('k2'); 
let o = { 
 1: 1, 
 first: 'first', 
 [k1]: 'sym2', 
 second: 'second', 
 0: 0 
}; 
o[k2] = 'sym2'; 
o[3] = 3; 
o.third = 'third'; 
o[2] = 2; 
console.log(Object.getOwnPropertyNames(o)); 
// ["0", "1", "2", "3", "first", "second", "third"] 
console.log(Object.getOwnPropertySymbols(o)); 
// [Symbol(k1), Symbol(k2)]

4. 对象迭代

ECMAScript 2017 新增了两个静态方法,用于将对象内容转换为序列化的,更重要的是可迭代的格式。这两个静态方法Object.values()和 Object.entries()接收一个对象,返回它们内容的数组。Object.values()返回对象值的数组,Object.entries()返回键/值对的数组

非字符串属性会被转换为字符串输出,符号属性会被忽略

这两个方法执行对象的浅复制

const o = {
  foo: 'bar',
  baz: 1,
  qux: {}
}
console.log(Object.values(o))// (3) ['bar', 1, {…}]
console.log(Object.entries(o))// [["foo", "bar"], ["baz", 1], ["qux", {}]]

// 执行对象的浅复制
console.log(Object.values(o)[2] === o.qux)// true
console.log(Object.entries(o)[2][1] === o.qux)// true

// 符号属性被忽略
const sym = Symbol()
const o1 = {
  [sym]: 'foo'
}
console.log(Object.values(o1))// (0) []
console.log(Object.entries(o1))// (0) []
1. 其它原型语法

简写:可以直接通过一个包含所有属性和方法对象字面量来重写原型

function Person(){}
Person.prototype = {
  name: "Matt",
  age: 17,
  job: "Doctor",
  sayName(){
    // 由于是在原型上定义,因此可以不用引用外部定义函数使其单一
    console.log(this.name)
  }
}

这样重写之后,Person.prototype 的 constructor 属性就不指向 Person了,constructor 属性也指向了完全不同的新对象(Object 构造函数),不再指向原来的构造函数。虽然 instanceof 操作符还能可靠地返回值,但我们不能再依靠 constructor 属性来识别类型了

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

如果 constructor 的值很重要,则可以像下面这样在重写原型对象时专门设置一下它的值,但要注意,以这种方式恢复 constructor 属性会创建一个[[Enumerable]]为 true 的属性。而原生 constructor 属性默认是不可枚举的。

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

使用 Object.defineProperty()方法来定义 constructor 属性,可以使其不可枚举

// 恢复 constructor 属性
Object.defineProperty(Person.prototype, "constructor", { 
 enumerable: false, 
 value: Person 
});
2. 原型的动态性

因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改也会在实例上反映出来。可以对原型上的属性进行修改,则修改前的实例还可以调用修改后的结果

实例的[[Prototype]]指针是在调用构造函数时自动赋值的,这个指针即使把原型修改为不同的对象也不会变重写整个原型(类似于将原型改为另外一个对象)会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型。记住,实例只有指向原型的指针,没有指向构造函数的指针

重写构造函数上的原型之后再创建的实例才会引用新的原型。而在此之前创建的实例仍然会引用最初的原型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FplhUjQn-1641778705789)(assert/8%E5%AF%B9%E8%B1%A1%E3%80%81%E7%B1%BB%E4%B8%8E%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%BC%96%E7%A8%8B.assert/1641715073805.png)]

function Person(){}
Person.prototype = {
  name: "Matt",
  age: 17,
  job: "Doctor",
  sayName(){
    // 由于是在原型上定义,因此可以不用引用外部定义函数使其单一
    console.log(this.name)
  }
}
// 进行修改
let person1 = new Person
Person.prototype.sayName = function(){
  console.log("hi")
}
person1.sayName()// "hi"    还可以进行调用

// 整个原型重写,此时在重写前定义的不能再调用重写后的内容
let person2 = new Person
Person.prototype = {
  name: "Diccy",
  age: 17,
  job: "Teacher",
  sayHi(){
    console.log('Hello')
  }
}
person2.sayHi()// Uncaught TypeError: person2.sayHi is not a function
3. 原生对象原型

原型模式之所以重要,不仅体现在自定义类型上,而且还因为它也是实现所有原生引用类型的模式。sort()方法就是 Array.prototype 上定义的,而字符串包装对象的 substring()方法也是在 String.prototype 上定义的

通过原生对象的原型可以取得所有默认方法的引用,也可以给原生类型的实例定义新的方法,随时可以添加方法

4. 原型的问题

它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值

原型上的所有属性是在实例间共享的,这对函数来说比较合适。另外包含原始值的属性也还好,可以通过在实例上添加同名属性来简单地遮蔽原型上的属性

真正的问题来自包含引用值的属性

function Person(){}
Person.prototype = {
  name: "Matt",
  age: 28,
  job: "Doctor",
  friends: ["a", "b", "c"]
}
let person1 = new Person
let person2 = new Person
person1.friends.push("D")
console.log(person1.friends)// (4) ['a', 'b', 'c', 'D']
console.log(person2.friends)// (4) ['a', 'b', 'c', 'D']
console.log(person1.friends === person2.friends)// true
// 对引用值的修改同时导致另外的实例上的值被修改
person1.name = "Ab"
console.log(person1.name)// Ab
console.log(person2.name)// Matt

3. 继承

继承:接口继承实现继承

接口继承:只继承方法签名,在ES中不可能

实现继承:继承实际的方法,通过原型链实现

1. 原型链

原型链基本思想就是通过原型继承多个引用类型的属性和方法,即原型可以是另一个类型的实例。原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数

// 父类
function SuperType() {
  this.superProperty = true
}
SuperType.prototype.getSuperValue = function(){
  return this.superProperty
}
// 子类
function SubType(){
  this.subProperty = false
}
// 继承SuperType
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function(){
  return this.subProperty
}
let instance = new SubType()
console.log(instance)
/**
 * SubType {subProperty: false}
 *  subProperty:false
 *  [[Prototype]]:SuperType
 *    getSubValue:ƒ (){\r\n  return this.subProperty\r\n}
 *    superProperty:true
 *      [[Prototype]]:Object
 */

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bzEEJcTV-1641778705790)(assert/8%E5%AF%B9%E8%B1%A1%E3%80%81%E7%B1%BB%E4%B8%8E%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%BC%96%E7%A8%8B.assert/1641717229996.png)]

这个例子中实现继承的关键,是 SubType 没有使用默认原型,而是将其替换成了一个新的对象。SubType.prototype 现在是 SuperType 的一个实例,因此 superProperty才会存储在它上面

SubType.prototype 的 constructor属性被重写为指向SuperType,所以 instance.constructor 也指向 SuperType

1. 默认原型

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xxFrON5A-1641778705790)(assert/8%E5%AF%B9%E8%B1%A1%E3%80%81%E7%B1%BB%E4%B8%8E%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%BC%96%E7%A8%8B.assert/1641717470153.png)]

2. 原型与继承关系
  • instanceof操作符,若一个实例的原型链出现过相应的构造函数,则instanceof返回true

    console.log(instance instanceof Object)// true
    console.log(instance instanceof SuperType)// true
    console.log(instance instanceof SubType)// true
    
  • isPrototypeOf()方法,原型链中的每个原型都可以调用这个方法,只要原型链中包含该原型,该方法返回true

    console.log(Object.prototype.isPrototypeOf(instance))
    console.log(SuperType.prototype.isPrototypeOf(instance))
    console.log(SubType.prototype.isPrototypeOf(instance))
    
3. 关于方法
  1. 子类有时候需要覆盖父类的方法,或者增加父类没有的方法。为此,这些方法必须在原型赋值之后再添加到原型

    function SuperType() {
      this.superProperty = true
    }
    SuperType.prototype.getSuperValue = function () {
      return this.superProperty
    }
    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
    
  2. 对象字面量方式创建原型方法会破坏之前的原型链,因为这相当于重写了原型链

    function SuperType() {
      this.superProperty = true
    }
    SuperType.prototype.getSuperValue = function () {
      return this.superProperty
    }
    function SubType() {
      this.subProperty = false
    }
    // 继承SuperType
    SubType.prototype = new SuperType()
    // 通过对象字面量添加新方法,则会导致之前继承的原型无效,因为[[Prototype]]指向改变了
    SubType.prototype = {
      getSubValue() {
        return this.subProperty
      },
      someOtherMethod() {
        return false
      }
    }
    let instance = new SubType()
    console.log(instance.getSuperValue())// Uncaught TypeError: instance.getSuperValue is not a function
    
4. 原型链的问题

原型中包含引用值的时候,对于所有的实例来说,这些引用值是共享的。而对于原型链来说,某一个被继承的实例则变成了一个原型,而原在该实例上的属性,即被变成了原型属性,当该实例上由引用值时,则继承其的实例都共享这个引用值

子类型在实例化的时候不能给父类型的构造函数传参,无法在不影响所有对象实例的情况下把参数传进父类的构造函数


2. 盗用构造函数

解决原型链问题出现的方法盗用构造函数constructor stealing,也称为对象伪装经典继承。思想即在子类构造函数中调用父类构造函数,因为函数是在特定上下文中执行代码的简单对象,可以使用**apply()call()方法以新创建的对象为上下文执行构造函数**

function SuperType(){
  this.colors = ["red", "blue", "green"]
}
function SubType(){
  // 继承SuperType
  SuperType.call(this)  // SuperType调用SubType执行构造函数
}
// 每个实例都有自己的colors属性
let instance1 = new SubType()
instance1.colors.push("black")
console.log(instance1.colors)// (4) ['red', 'blue', 'green', 'black']
let instance2 = new SubType()
console.log(instance2.colors)// (3) ['red', 'blue', 'green']
1. 传递参数

使用盗用构造函数可以在子类构造函数中向父类构造函数传参,为了避免出现父类构造函数覆盖子类定义的属性情况,则可以在调用父类构造函数之后再给子类实例添加额外的属性

function SuperType(name) {
  this.name = name
}
function SubType(){
  // 继承SuperType并传参
  SuperType.call(this, "Matt")

  // 实例属性
  this.age = 27
}
let instance = new SubType()
console.log(instance.name)// Matt
console.log(instance.age)// 27
2. 盗用构造函数的问题
function SuperType(name) {
  this.name = name
}
SuperType.prototype.sayHi = function(){
  console.log('Hi')
}
function SubType(){
  // 继承SuperType并传参
  SuperType.call(this, "Matt")
}
let instance = new SubType()
console.log(instance.sayHi())// Uncaught TypeError: instance.sayHi is not a function

必须在构造函数中定义方法,函数不能重用。子类不能访问父类原型上定义的方法,所有类型只能使用构造函数模式


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("Matt", 27)
instance1.colors.push("black")
console.log(instance1.colors)// (4) ['red', 'blue', 'green', 'black']
instance1.sayName()// Matt
instance1.sayAge()// 27

let instance2 = new SubType("Greg", 29)
console.log(instance2.colors)// (3) ['red', 'blue', 'green']
instance2.sayName()// Greg
instance2.sayAge()// 29

组合继承保留了 instanceof操作符和 isPrototypeOf()方法识别合成对象的能力


4. 原型式继承

介绍了一种不涉及严格意义上构造函数的继承方法,即即使不自定义类型也可以通过原型实现对象之间的信息共享

以下object()方法创建一个临时构造函数,将传入的对象赋值给这个构造函数原型,返回这个临时类型的实例,实际上是对传入对象进行了一次浅复制

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

ECMAScript 5 通过增加 Object.create()方法将原型式继承的概念规范化了。这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。在只有一个参数时,Object.create()与这里的 object()方法效果相同

 name: "Nicholas", 
 friends: ["Shelby", "Court", "Van"] 
}; 
let anotherPerson = Object.create(person); 
anotherPerson.name = "Greg"; 
anotherPerson.friends.push("Rob"); 
let yetAnotherPerson = Object.create(person); 
yetAnotherPerson.name = "Linda"; 
yetAnotherPerson.friends.push("Barbie"); 
console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"

Object.create()的第二个参数与 Object.defineProperties()的第二个参数一样:每个新增属性都通过各自的描述符来描述。以这种方式添加的属性会遮蔽原型对象上的同名属性

let person = { 
 name: "Nicholas", 
 friends: ["Shelby", "Court", "Van"] 
}; 
let anotherPerson = Object.create(person, { 
 name: { 
 value: "Greg" 
 } 
}); 
console.log(anotherPerson.name); // "Greg"

5. 寄生式继承

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

function TryParasitic(obj) {
  let clone = Object.create(obj)// 创建对象
  clone.sayHi = function(){     // 增强对象,实际上与给Object.create传入第二参数效果相同
    console.log('Hi')
  }
  return clone                  // 返回对象
}

6. 寄生式组合继承

子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型即可

组合继承原理:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eKsfj0zy-1641778705790)(assert/8%E5%AF%B9%E8%B1%A1%E3%80%81%E7%B1%BB%E4%B8%8E%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%BC%96%E7%A8%8B.assert/1641730709025.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1t3Nz08w-1641778705791)(assert/8%E5%AF%B9%E8%B1%A1%E3%80%81%E7%B1%BB%E4%B8%8E%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%BC%96%E7%A8%8B.assert/1641730721438.png)]

寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。寄生式组合继承的基本模式如下所示

function inheritPrototype(subType, superType) {
  let prototype = Object.create(superType.prototype)// 创建对象
  prototype.constructor = subType                   // 增强对象
  subType.prototype = prototype                     // 赋值对象
}
function SuperType(name) { 
  this.name = name
  this.colors = ["red", "blue", "green"]
} 
SuperType.prototype.sayName = function() { 
  console.log(this.name)
}; 
function SubType(name, age) { 
  SuperType.call(this, name)                        // 盗用构造函数继承属性
  this.age = age
}
inheritPrototype(SubType, SuperType)                // 继承方法
SubType.prototype.sayAge = function () {
  console.log(this.age)
}

以上函数即

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y0A9USEZ-1641778705791)(assert/8%E5%AF%B9%E8%B1%A1%E3%80%81%E7%B1%BB%E4%B8%8E%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%BC%96%E7%A8%8B.assert/1641731428611.png)]

原型链仍然保持不变,因此 instanceof操作符和isPrototypeOf()方法正常有效


4. 类

ES6表面上支持正式的面向对象编程,实际上仍然是原型和构造函数的概念

1. 类定义

定义类的方式以下两种,都需要class关键字加大括号

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

类表达式在被求值前不能引用,即使是var也会返回undefined;函数声明可以提升,类定义不能提升

console.log(FunctionExpression); // undefined 
var FunctionExpression = function() {}; 
console.log(FunctionExpression); // function() {} 
console.log(FunctionDeclaration); // FunctionDeclaration() {} 
function FunctionDeclaration() {} 
console.log(FunctionDeclaration); // FunctionDeclaration() {} 
console.log(ClassExpression); // undefined 
var ClassExpression = class {}; 
console.log(ClassExpression); // class {} 
console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined 
class ClassDeclaration {} 
console.log(ClassDeclaration); // class ClassDeclaration {}

函数受函数作用域限制,类受块作用域限制

{ 
 function FunctionDeclaration() {} 
 class ClassDeclaration {} 
} 
console.log(FunctionDeclaration); // FunctionDeclaration() {} 
console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined
2. 类的构成

类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法,但这些都不是必需的,空的类定义照样有效;建议类名的首字母要大写,以区别于通过它创建的实例

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

类表达式的名称是可选的。在把类表达式赋值给变量后,可以通过 name属性取得类表达式的名称字符串。但不能在类表达式作用域外部访问这个标识符

let Person = class PersonName {
  identity() {
    console.log(Person.name, PersonName.name)
  }
}
let p = new Person()
p.identity()// PersonName PersonName
console.log(p.name)// undefined
console.log(Person.name)// PersonName
console.log(PersonName.name)// Uncaught ReferenceError: PersonName is not defined

2. 类构造函数

constructor关键字用于在类定义块内部创建类的构造函数

1. 实例化

使用newconstructor对类进行实例化;使用new调用类的构造函数会执行如下操作:

  • 在内存中创建一个对象
  • 在新对象内部的[[Prototype]]指向构造函数的prototype属性
  • this指向新对象
  • 执行构造函数内部的代码,即给新对象添加属性
  • 若构造函数返回非空对象,则返回该非空对象,否则返回刚创建的新对象

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

默认情况下,类构造函数会在执行之后返回 this对象。如果返回的不是 this对象,而是其他对象,那么这个对象不会通过 instanceof操作符检测出跟类有关联,因为这个对象的原型指针并没有被修改

class Person { 
 constructor(override) { 
 this.foo = 'foo'; 
 if (override) { 
 return { 
 bar: 'bar' 
 }; 
 } 
 } 
} 
let p1 = new Person(), 
 p2 = new Person(true); 
console.log(p1); // Person{ foo: 'foo' } 
console.log(p1 instanceof Person); // true 
console.log(p2); // { bar: 'bar' } 
console.log(p2 instanceof Person); // false

类构造函数与构造函数的主要区别是,调用类构造函数必须使用 new操作符。而普通构造函数如果不使用 new调用,那么就会以全局的 this(通常是 window)作为内部对象

类构造函数没有什么特殊之处,实例化之后,它会成为普通的实例方法(但作为类构造函数,仍然要使用 new调用)。因此,实例化之后可以在实例上引用它

class Person {}
// 使用类创建一个新实例
let p1 = new Person
p1.constructor()// Uncaught TypeError: Class constructor Person cannot be invoked without 'new'
// 使用对类构造函数的引用创建一个新实例
let p2 = new p1.constructor()
console.log(p1)// Person
console.log(p2)// Person
2. 把类当成特殊函数

与函数定义相似


3. 实例、原型和类成员

每个实例都对应一个唯一的成员对象,这意味着所有成员都不会在原型上共享

为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法,在类块中定义的所有内容都会定义在类的原型

class Person { 
 constructor() { 
 // 添加到 this 的所有内容都会存在于不同的实例上
 this.locate = () => console.log('instance'); 
 }
 // 在类块中定义的所有内容都会定义在类的原型上
 locate() { 
 console.log('prototype'); 
 } 
} 
let p = new Person(); 
p.locate(); // instance 
Person.prototype.locate(); // prototype

可以把方法定义在类构造函数中或者类块中,但不能在类块中给原型添加原始值或对象作为成员数据

class Person { 
 name: 'Jake' 
} 
// Uncaught SyntaxError: Unexpected token
静态类成员

静态方法通常用于执行不特定于实例的操作,也不要求存在类的实例

每个类上只能由一个静态成员,在类定义中使用static作为前缀,this引用类自身

class Person { 
 constructor() { 
 // 添加到 this 的所有内容都会存在于不同的实例上
 this.locate = () => console.log('instance', this); 
 } 
 // 定义在类的原型对象上
 locate() { 
 console.log('prototype', this); 
 } 
 // 定义在类本身上
 static locate() { 
 console.log('class', this); 
 } 
} 
let p = new Person(); 
p.locate(); // instance, Person {} 
Person.prototype.locate(); // prototype, {constructor: ... } 
Person.locate(); // class, class Person {}

4. 继承

1. 继承基础

使用 extends关键字,就可以继承任何拥有[[Construct]]和原型的对象。很大程度上,这意味着不仅可以继承一个类,也可以继承普通的构造函数

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

派生类都会通过原型链访问到类和原型上定义的方法。this的值会反映调用相应方法的实例或者类

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

参考Page259

3. 抽象基类

抽象基类可供其他类继承,但本身不会被实例化,通过new.target保存new关键字调用的类或函数可以实现。通过在实例化时检测new.target是不是抽象基类,可以阻止对抽象基类的实例化

// 抽象基类 
class Vehicle { 
  constructor() { 
    console.log(new.target); 
    if (new.target === Vehicle) { // 本案例中抽象基类是Vehicle,当当前目标是其时,丢出错误
      throw new Error('Vehicle cannot be directly instantiated');
    } 
  } 
} 
// 派生类
class Bus extends Vehicle {} 
new Bus(); // class Bus {} 
new Vehicle(); // class Vehicle {} 
// Error: Vehicle cannot be directly instantiated

通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。因为原型方法在调用类构造函数之前就已经存在了,所以可以通过 this关键字来检查相应的方法

// 抽象基类
class Vehicle {
  constructor(){
    if(new.target === Vehicle){
      throw new Error('Vehicle cannot be directly instantiated')
    }
    if(!this.foo){// 即this上需要由foo方法
      throw new Error('Inheriting class must define foo()')
    }
    console.log('success!') // 满足所有条件
  }
}
// 派生类
class Bus extends Vehicle {
  foo() {}
}
class Van extends Vehicle {}
new Bus()// success!
new Van()// Uncaught Error: Inheriting class must define foo()
4. 继承内置类型

即在类中添加方法,有些内置类型的方法会返回新实例。默认情况下,返回实例类型与原始实例类型一致。若想覆盖该行为,可以覆盖Symbol.species访问器,该访问器决定在创建返回的实例时使用的类

class SuperArray extends Array { 
 static get [Symbol.species]() { 
 return Array; 
 } 
} 
let a1 = new SuperArray(1, 2, 3, 4, 5); 
let a2 = a1.filter(x => !!(x%2)) 
console.log(a1); // [1, 2, 3, 4, 5] 
console.log(a2); // [1, 3, 5] 
console.log(a1 instanceof SuperArray); // true 
console.log(a2 instanceof SuperArray); // false
5. 类混入

不同类的行为集中到一个类即类混入

Object.assign()方法是为了混入对象行为而设计的

extends 关键字后面是一个 JavaScript 表达式。任何可以解析为一个类或一个构造函数的表达式都是有效的

混入模式可以通过在一个表达式中连缀多个混入元素来实现,这个表达式最终会解析为一个可以被继承的类

一个策略是定义一组“可嵌套”的函数,每个函数分别接收一个超类作为参数,而将混入类定义为这个参数的子类,并返回这个类。这些组合函数可以连缀调用,最终组合成超类表达式

class Vehicle {} 
let FooMixin = (Superclass) => class extends Superclass { 
 foo() { 
  console.log('foo'); 
 } 
}; 
let BarMixin = (Superclass) => class extends Superclass { 
 bar() { 
  console.log('bar'); 
 } 
}; 
let BazMixin = (Superclass) => class extends Superclass { 
 baz() { 
  console.log('baz'); 
 } 
}; 
class Bus extends FooMixin(BarMixin(BazMixin(Vehicle))) {} 
let b = new Bus(); 
b.foo(); // foo 
b.bar(); // bar 
b.baz(); // baz

通过写一个辅助函数,可以把嵌套调用展开

class Vehicle {} 
let FooMixin = (Superclass) => class extends Superclass { 
 foo() { 
 console.log('foo'); 
 } 
}; 
let BarMixin = (Superclass) => class extends Superclass { 
 bar() { 
 console.log('bar'); 
 } 
}; 
let BazMixin = (Superclass) => class extends Superclass { 
 baz() { 
 console.log('baz'); 
 } 
}; 
function mix(BaseClass, ...Mixins) { 
 return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass); 
} 
class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {} 
let b = new Bus(); 
b.foo(); // foo 
b.bar(); // bar 
b.baz(); // baz

s Vehicle {
foo() {}
}
class Van extends Vehicle {}
new Bus()// success!
new Van()// Uncaught Error: Inheriting class must define foo()


#### 4. 继承内置类型

即在类中添加方法,有些内置类型的方法会返回新实例。默认情况下,返回实例类型与原始实例类型一致。若想覆盖该行为,可以覆盖Symbol.species访问器,该访问器决定在创建返回的实例时使用的类

```js
class SuperArray extends Array { 
 static get [Symbol.species]() { 
 return Array; 
 } 
} 
let a1 = new SuperArray(1, 2, 3, 4, 5); 
let a2 = a1.filter(x => !!(x%2)) 
console.log(a1); // [1, 2, 3, 4, 5] 
console.log(a2); // [1, 3, 5] 
console.log(a1 instanceof SuperArray); // true 
console.log(a2 instanceof SuperArray); // false
5. 类混入

不同类的行为集中到一个类即类混入

Object.assign()方法是为了混入对象行为而设计的

extends 关键字后面是一个 JavaScript 表达式。任何可以解析为一个类或一个构造函数的表达式都是有效的

混入模式可以通过在一个表达式中连缀多个混入元素来实现,这个表达式最终会解析为一个可以被继承的类

一个策略是定义一组“可嵌套”的函数,每个函数分别接收一个超类作为参数,而将混入类定义为这个参数的子类,并返回这个类。这些组合函数可以连缀调用,最终组合成超类表达式

class Vehicle {} 
let FooMixin = (Superclass) => class extends Superclass { 
 foo() { 
  console.log('foo'); 
 } 
}; 
let BarMixin = (Superclass) => class extends Superclass { 
 bar() { 
  console.log('bar'); 
 } 
}; 
let BazMixin = (Superclass) => class extends Superclass { 
 baz() { 
  console.log('baz'); 
 } 
}; 
class Bus extends FooMixin(BarMixin(BazMixin(Vehicle))) {} 
let b = new Bus(); 
b.foo(); // foo 
b.bar(); // bar 
b.baz(); // baz

通过写一个辅助函数,可以把嵌套调用展开

class Vehicle {} 
let FooMixin = (Superclass) => class extends Superclass { 
 foo() { 
 console.log('foo'); 
 } 
}; 
let BarMixin = (Superclass) => class extends Superclass { 
 bar() { 
 console.log('bar'); 
 } 
}; 
let BazMixin = (Superclass) => class extends Superclass { 
 baz() { 
 console.log('baz'); 
 } 
}; 
function mix(BaseClass, ...Mixins) { 
 return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass); 
} 
class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {} 
let b = new Bus(); 
b.foo(); // foo 
b.bar(); // bar 
b.baz(); // baz
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值