不能取得类pictrure的insert属性_对象、类与面向对象编程

认识对象

MDN:面向对象编程是用抽象方式创建基于现实世界模型的一种编程模式。它使用先前建立的范例,包括模块化,多态和封装几种技术。

任何一个对象都是唯一的,这与它本身的状态无关

我们用状态来描述对象

状态的改变即是行为

面向对象三大特性

  • 封装 -- 封装,复用,解耦,内聚(描述架构)
  • 继承 -- Class Base 面向对象
  • 多态 -- 描述动态性的程度

Class vs Prototype

Class(类)

  • 类是一种常见的描述对象的方式
  • “归类” 和 “分类” 两个主要流派
  • 对于归类,多继承 C++
  • 采用分类思想的计算机语言,单继承。并且会有一个基类 Object

Prototype(原型)

  • 原型是一种更接近人类原始认知的描述对象的方式
  • 我们并不试图做严谨的分类,而是采用 “相似” 这样的方法来描述对象
  • 任何对象仅仅需要描述它自己与原型的区别即可

JavaScript 对象

ECMA-262 将对象定义为一组属性的无序集合

对象的每个属性或方法都由一个名称来标识,这个名称映射到一个值

可以把 ECMAScript 的对象想象成一张散列表,其中的内容就是一组 名/值 对,值可以是数据或者函数

属性类型

ECMA-262 使用一些内部特性来描述属性的特征。这些特性是由为 JavaScript 实现引擎的规范定义。因此,开发者不能在 JavaScript 中直接访问这些特性

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

数据属性

数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。

数据属性有 4 个特性描述它们的行为

  • [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true
  • [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true
  • [[Writable]]:表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的这个特性都是 true
  • [[value]]:包含属性实际的值
let person = {
  name: 'andy'
}

在上面例子中,会将属性显式添加到对象之后,[[Configurable]][[Enumerable]][[Writable]] 都会被设置为 true,而 [[Value]] 特性会被设置为指定的值

这里,我们创建了一个名为 name 的属性的对象,并赋值 'andy' 。这意味着 [[value]] 特性会被设置为 'andy' ,之后对这个值的任何修改都会保存到这个位置

如果想要修改属性的默认特性,必须使用 Object.defineProperty(obj, prop, descriptor) 方法

这个方法接收三个参数

  • obj :要定义属性的对象
  • prop :要定义或修改的属性的名称或 Symbol
  • descriptor :要定义或修改的属性描述符
let person = {}
Object.defineProperty(person, 'name', {
  writable: false,
  configurable: false,
  enumerable: false,
  value: 'andy'
})

注意:

  • 当设置 writablefalse ,这个属性的值就不能再被修改。当尝试修改这个属性时,在非严格模式下会被忽略,在严格模式下会抛出错误
  • 当设置 configurablefalse ,这个属性就不能从对象上删除。当尝试对这个属性调用 delete 删除,在非严格模式下会被忽略,在严格模式下会抛出错误。
  • 当一个属性被定义为不可配置之后,就不能再变回可配置,再次调用 Object.defineProperty() 并修改任何非 writable 属性会导致错误
  • 在调用 Object.defineProperty() 时,configurableenumerablewritable 的值如果不指定,都默认为 false

访问器属性

包含一个获取 getter 函数和一个设置 setter 函数,不过这两个函数不是必需的。
在读取访问器属性时,会调用获取函数 getter ,这个函数的责任就是返回一个有效的值。
在写入访问器属性时,会调用设置函数 setter 并传入新值,这个函数必须决定对数据做出什么修改。

访问器属性有 4 个特性描述它们的行为

  • [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true
  • [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true
  • [[Get]]:获取函数,在读取属性时调用。默认值为 undefined
  • [[Set]]:设置函数,在读取属性时调用。默认值为 undefined

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

let perosn = {
  _age: 18
}

Object.defineProperty(person, 'age', {
  get() {
    return this._age
  }
  set(newValue) {
   if(newValue > 18) {
      this._age = newValue
    }
 }
})

定义多个属性

ECMAScript 提供了 Object.defineProperties(obj, props) 方法,通过多个描述符一次性定义多个属性

接收两个参数:

  • obj:要定义或修改属性的对象
  • props:要定义其可枚举属性或修改的属性描述符的对象

读取属性的特性

ECMAScript 提供了 Object.getOwnPropertyDescriptor(obj, prop) 方法,可以取得制定属性的属性描述符

接收两个参数:

  • obj : 需要查找的目标对象
  • prop :目标对象内属性名称

返回值:

如果指定的属性存在于对象上,则返回其属性描述符对象(property descriptor),否则返回 undefined

ECMAScript 2017 新增了 Object.getOwnPropertyDescriptors(obj) 方法,用来获取一个对象的所有自有属性的描述符

接收一个参数:

  • obj :任意对象

返回值:

所指定对象的所有自有属性的描述符,如果没有任何自有属性,则返回空对象

这个方法实际上会在每个自有属性上调用 Object.getOwnPropertyDescriptor() 并在一个新对象中返回它们

Object 的一些扩展

Object.assign

ECMAScript6 提供了 Object.assign(target, ...sources) 方法,将所有可枚举属性的值从一个或多个源对象分配到目标对象。

参数:

  • target :目标对象
  • sources :源对象

返回值:

目标对象

注意:

如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖;后面的源对象的属性将类似地覆盖前面的源对象的属性

Object.assign 方法只会拷贝源对象自有(Object.hasOwnProperty 返回 true)的并且可枚举(Object.propertyIsEnumbeable 返回 true)的属性到目标对象

Stirng 类型和 Symbol 类型的属性会被拷贝

这个方法会使用源对象上的 [[Get]] 取得属性的值,然后使用目标对象上的 [[Set]] 设置属性的值

Object.assign() 实际上对每个源对象执行的是浅拷贝

如果赋值期间出错,则操作会终止并退出,同时抛出错误;Object.assign() 没有 “回滚” 之前赋值的概念,因此它是一个尽力而为、可能只会完成部分拷贝的方法

let dest = {}
let src = { a: 'foo' }
let result = Object.assign(dest, src)

console.log(dest === result) // true
console.log(dest !== src) // true
console.log(result) // {a: "foo"}

Object.is

Object.is() 方法判断两个值是否为同一个值
Object.is(+0, 0) // true
Object.is(-0, 0) // false
Object.is(NaN, NaN) // true

// Polyfill

if (!Object.is) {
  Object.is = function (x, y) {
    if (x === y) {
      return x !== 0 || 1 / x === 1 / y
    } else {
      return x !== x && y !== y
    }
  }
}

创建对象

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

ES6 之前没有正式支持面向对象的结构,比如类和继承。我们可以运用原型继承来模拟同样的行为(不推荐)

ES6 开始正式支持类和继承。ES6 的类旨在完全涵盖之前规范设计的基于原型的继承模式。不过,无论从哪方面看,ES6 的类都仅仅是封装了 ES5.1 构造函数加原型继承的语法糖而已

工厂模式

用于抽象创建特定对象的过程

function createPerson(name, age) {
  let o = new Object()

  o.name = name
  o.age = age
  o.sayName = function () {
    console.log(this.name)
  }

  return o
}

let person = createPerson('Andy', 18)

虽然解决了创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)

构造函数模式

ECMAScript 中的构造函数是用于创建特定类型对象的

function Person(name, age) {
  this.name = name
  this.age = age
  this.sayName = function () {
    console.log(this.name)
  }
}

let person = new Person('Andy', 18)

这个例子中, Person() 构造函数代替了 createPerson() 工厂函数。实际上,内部代码基本一致,有如下区别:

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

要创建 Person 的实例,应使用 new 操作符。以这种方式调用构造函数会执行如下操作:

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

构造函数不一定写成函数声明的形式;赋值给变量的函数表达式也可以:

let Person = function(name, age) {
  // ...
}

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

function Person() {
 this.name = 'Andy'
  this.sayName = function() {
    console.log(this.name)
  }
}

let p1 = new Person()
let p2 = new Person

1. 构造函数也是函数

构造函数与普通函数唯一的区别就是调用方式不同

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

2. 构造函数的问题

其定义的方法会在每个实例上都创建一遍

在上面的例子中,p1p2 都有名为 sayName() 的方法,但这两个方法不是同一个 Function 实例

在 ECMAScript 中的函数时对象,因此每次定义函数时,都会初始化一个对象

function Person() {
  this.name = 'Andy'
  this.sayName = new Function('console.log(this.name)') // 逻辑等价
}

console.log(p1.sayName == p2.sayName) // false

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

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

function Person() {
  this.name = 'Andy'
  this.sayName = sayName
}

function sayName() {
  console.log(this.name)
}

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

原型模式

每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用的实例共享的属性和方法

实际上,这个对象就是通过调用构造函数创建的对象的原型

function Person() { }

Person.prototype.name = 'Andy'
Person.prototype.sayName = function () {
  console.log(this.name)
}

let p1 = new Person
p1.sayName() // "Andy"

let p2 = new Person()
p2.sayName() // "Andy"

console.log(p1.sayName === p2.sayName) // true

1. 理解原型

无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性(指向原型对象)

默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构造函数

可以给原型对象添加其他属性和方法

Person.prototype.constructor === Person // true

在自定义构造函数时,原型对象默认只会获得 constructor 属性,其他的所有方法都继承自 Object

每次调用构造函数创建一个新实例,这个实例的内部 [[prototype]] 指针就会被赋值为构造函数的原型对象

脚本中没有访问这个 [[prototype]] 特性的标准方式,Firfox、Safari 和 Chrome 会在每个对象上暴露 __proto__ 属性,通过这个属性可以访问对象的原型

关键点:实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有

/**
 * 构造函数可以是函数表达式,也可以是函数声明
 *  function Person() {}
 *  let Person = function() {}
 */
function Person() { }

/**
 * 声明之后,构造函数就有了一个与之关联的原型对象
 */
console.log(typeof Person.prototype)
console.log(Person.prototype)
// {
//   constructor: f Person(),
//   __proto__: 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

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

/**
 * 实例通过 __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

0e7490184296f22fdca24e6134a4dbda.png

虽然不是所有实现都对外暴露了 [[Prototype]] ,但可以使用 isPrototypeOf() 方法确定两个对象之间的这种关系。本质上,isPrototype() 会比较传入参数的 [[Prototype]] 是否指向调用它的对象

Person.prototype.isPrototypeOf(person1) // true
Person.prototype.isPrototypeOf(person2) // true

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

Object.getPrototypeOf(person1) === Person.prototype // true

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

let biped = {
  numLegs: 2
}

let person = {
  name: 'Matt'
}

Object.setPrototypeOf(person, biped)

console.log(person.numLegs) // 2
console.log(Object.getPrototypeOf(person) === biped) // true

注意:Object.setPrototypeOf() 可能会严重影响代码性能。Mozilla 文档中指出:

在所有浏览器和 JavaScript 引擎中,修改继承关系的影响都是微妙且深远的。这种影响并不仅是执行 Object.setPrototypeOf() 语句那么简单,而是会涉及所有访问了哪些修改过 [[Prototype]] 的对象的代码。

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

let biped = {
  numLegs: 2
}

let person = Object.create(biped)

console.log(person.numLegs) // 2
console.log(Object.getPrototypeOf(person) === biped) // true

2. 原型层级

读取实例的属性时,首先会在实例上搜索这个属性;如果没有找到,则会继承搜索实例的原型

实例可以读取原型对象上的值

实例不可重写这些值

如果在实例上添加一个与原型对象中同名的属性,会在实例上创建这个属性,这个属性会遮蔽(shadow)原型对象上的属性

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

function Person() {}

Person.prototype.name = 'Nicholas'
Person.prototype.age = 18
Person.prototype.sayName = function() {
  console.log(this.name)
}

let person1 = new Person
let person2 = new Person

person1.name = 'Andy'
console.log(person1.name) // "Andy"
console.log(person2.name) // "Nicholas"

delete person1.name
console.log(person1.name) // "Nicholas"

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

这个方法是继承自 Object ,会在属性存在于调用它的对象实例上时返回 true

function Person() {}

Person.prototype.name = 'Nicholas'
Person.prototype.age = 18
Person.prototype.sayName = function() {
  console.log(this.name)
}

let person1 = new Person
let person2 = new Person
console.log(person1.hasOwnProperty('name')) // false

person1.name = 'Andy'
console.log(person1.name) // "Andy"
console.log(person1.hasOwnProperty('name')) // true

console.log(person2.name) // "Nicholas"
console.log(person2.hasOwnProperty('name')) // false

delete person1.name
console.log(person1.name) // "Nicholas"
console.log(person1.hasOwnProperty('name')) // false

5f74bdf55dfff516a191273a9caf31fb.png

3. 原型和 in 操作符

in 操作符有两种使用方式:单独使用和 for-in 循环

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

function Person() {}

Person.prototype.name = 'Nicholas'
Person.prototype.age = 18
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 = 'Andy'
console.log(person1.name) // "Andy"
console.log(person1.hasOwnProperty('name')) // true
console.log('name' in  person1) // true

console.log(person2.name) // "Nicholas"
console.log(person2.hasOwnProperty('name')) // false
console.log('name' in  person2) // true

delete person1.name
console.log(person1.name) // "Nicholas"
console.log(person1.hasOwnProperty('name')) // false
console.log('name' in  person1) // true

如果要判断某个属性是否存在于原型上?

function hasPrototypeProperty(obj, key) {
  return !obj.hasOwnProperty(key) && (key in obj)
}

只要对象可以访问, in 操作符就返回 true ,而 hasOwnProperty() 只有属性存在于实例上时才返回 true 。因此,只要 in 操作符返回 truehasOwnProperty() 返回 false ,就说明该属性是一个原型属性

for-in 循环中使用 in 操作符,可以通过对象访问且可以被枚举([[Enumerable]] 特性为 true)的属性都会返回,包括实例属性和原型属性

可以通过 Object.keys() 方法获取对象上所有可枚举的实例属性

可以通过 Object.getOwnPropertyNames() 方法获取所有实例属性

function Person() {}

Person.prototype.name = 'Nicholas'
Person.prototype.age = 18
Person.prototype.sayName = function() {
  console.log(this.name)
}

let keys = Object.keys(Person.prototype)
console.log(keys) // ["name", "age", "sayName"]
let p1 = new Person
p1.name = "Andy"
p1.age = 20
let p1keys = Object.keys(p1)
console.log(p1keys) // ["name", "age"]

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

在 ECMAScript6 新增符号(Symbol)类型后,相应地增加了 Object.getOwnPropertySymbols() 方法

let k1 = Symbol('k1')
let k2 = Symbol('k2')

let o = {
  [k1]: 'k1',
  [k2]: 'k2',
}

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

4. 属性枚举顺序

for-inObject,keys() 的枚举顺序是不确定的,取决于 JavaScript 引擎

Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.assign() 的枚举顺序是确定的。先以升序枚举数值键,然后以插入顺序枚举字符串和符号键

5. 原型相关语法

ECMAScript 2017 新增两个静态方法,用于将对象内容转换为序列化的 -- 更重要的是可迭代的 -- 格式

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

Object.entries 返回键/值对数组

const obj = {
  foo: 'bar',
  baz: 2,
  qux: {}
}

console.log(Object.values(obj)) // ["bar", 2, {}]
console.log(Object.entries(obj)) // [["foo", "bar"], ["baz", 2], ["qux", {}]]

非字符串属性会被转换为字符串输出。

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

const obj = {
  qux: {}
}

console.log(Object.values(obj)[0] === obj.qux) // true
console.log(Object.entries(obj)[0][1] === obj.qux) // true

符号属性会被忽略

const k = Symbol()
const obj = {
  [k]: 'foo'
}

console.log(Object.values(obj)) // []
console.log(Object.entries(obj)) // []

6. 原型的动态性

因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改都会在实例上反映出来

实例和原型之间的链接就是简单的指针,而不是保存的副本

重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型

实例只有指向原型的指针,没有指向构造函数的指针

function Person() {}

let p = new Person()

Person.prototype = {
  constructor: Person,
  name: 'Andy',
  age: 18,
  sayName() {
    console.log(this.name)
  }
}

p.sayName() // Uncaught TypeError: p.sayName is not a function

628dd55eccd893b263a51b6f163f25cd.png

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

7. 原生对象原型

所有原生引用类型的构造函数(包括 ObjectArrayString 等)都在原型上定义了实例方法

通过原生对象的原型可以取得所有默认方法的引用,也可以给原生类型的实例定义新的方法。可以像修改自定义对象原型一样修改原生对象原型

注意:尽管可以这么做,但并不推荐再生产环境中修改原生对象原型。这样做很可能造成误会,而且可能引发命名冲突。还有可能意外重写原生的方法。推荐的做法是创建一个自定义的类,继承原生类型

8. 原型的问题

弱化了向构造函数传递初始化参数的能力,会导致所有实例默认取得相同的属性值。虽然这会带来不便,但这不是原型最大的问题。原型最主要的问题源自它的共享特性。

原型上的所有属性是在实例间共享,这对函数来说比较合适。真正的问题来自包含引用值的属性

function Person() {}

Person.prototype = {
  constructor: Person,
  name: 'Andy',
  age: 18,
  friends: ['Lyn', 'Mike'],
  sayName() {
    console.log(this.name)
  }
}

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

person1.friends.push('Tony')

console.log(person1.friends) // ["Lyn", "Mike", "Tony"]
console.log(person2.friends) // ["Lyn", "Mike", "Tony"]
console.log(person1.friends === person2.friends) // true

手写实现 new

function newFunc(...args) {
  // 取出 args 数组的第一个参数,即目标构造函数
  const constructor = args.shift()

  // 创建一个空对象,并使这个对象继承构造函数的 prototype 属性
  // obj.__proto__ === constructor.prototype
  const obj = Object.create(constructor.prototype)

  // 执行构造函数,得到构造函数返回结果
  // 这里使用 apply 使构造函数内的 this 指向 obj
  const result = constructor.apply(obj, args)

  // 如果构造函数执行后,返回的结果是非空对象,则直接返回该结果,否则返回 obj
  return (typeof result === 'object' && result !== null) ? result : obj
}

继承

原型链

ECMA-262 把原型链定义为 ECMAScript 的主要继承方式。其基本思想就是通过原型继承多个引用类型的属性和方法

构造函数、原型和实例的关系:

  • 每个构造函数都有一个原型对象,原型有一个属性指回构造函数
  • 实例有一个内部指针指向原型

如果原型是另一个类型的实例呢?

那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就走实例和原型之间构造了一条原型链

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
}

let instance = new SubType()
console.log(instance.getSuperValue()) // true

83f46a8aaf001f20a5ad69d9faf20ee4.png

原型链扩展了原型搜索机制。我们知道,在读取实例上的属性时,首先会在实例上搜索这个属性;如果没找到,则会继承搜索实例的原型;在通过原型链实现继承之后,搜索就可以继承向上,搜索原型的原型。在这个例子中,调用 instance.getSuperValue() 经过了 3 步搜索:instanceSubType.prototypeSuperType.prototype ,最后一步才找到这个方法

1. 默认原型

默认情况下,所以引用类型都继承自 Object,这也是通过原型链实现的。

任何函数的默认原型都是 Object 的一个实例,这意味着这个实例有一个内部指针指向 Object.prototype

8f36c75514e68096cf4647795bfb96b1.png

2. 原型与实例的关系

原型与实例的关系可以通过两种方式来确定

  • instanceof 操作符:如果一个实例的原型链中出现过相应的构造函数,则 instanceof 返回 true
instance instanceof Object // true
instance instanceof SuperType // true
instance instanceof SubType // true
  • isPrototypeOf() 方法:原型链中的每个原型都可以调用这个方法,只有原型链中包含这个原型,就返回 true
Object.prototype.isPrototypeOf(instance) // true
SuperType.prototype.isPrototypeOf(instance) // true
SubType.prototype.isPrototypeOf(instance) // true
  • 以对象字面量方式创建原型方法会破坏之前的原型链,因为这相当于重写了原型链
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 = {
  getSubValue() {
    return this.subproperty;
  },

  someOtherMethod() {
    return false;
  }
}

let instance = new SubType()
console.log(instance.getSuperValue()) // Uncaught TypeError: instance.getSuperValue is not a function

3. 原型链的问题

  • 主要问题出现在原型中包含引用值的时候,原型中包含的引用值会在所有实例间共享
function SuperType() {
  this.colors = ['red', 'green', 'blue']
}

function SubType() {}

SubType.prototype = new SuperType()

let instance1 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors) // ["red", "green", "blue", "black"]

let instance2 = new SubType()
console.log(instance2.colors) // ["red", "green", "blue", "black"]
  • 子类型在实例化时不能给父类型的构造函数传参

经典继承(盗用构造函数)

在子类构造函数中调用父类构造函数

函数就是在特定上下文中执行代码的简单对象,所以可以使用 apply()call() 方法以新创建的对象上下文执行构造函数

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

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

let instance1 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors) // ["red", "green", "blue", "black"]

let instance2 = new SubType()
console.log(instance2.colors) // ["red", "green", "blue"]

1. 传递参数

function SuperType(name) {
  this.name = name
}

function SubType() {
  SuperType.call(this, 'Andy')
  this.age = 18
}

let instance = new SubType
console.log(instance.name) // "Andy"
console.log(instance.age) // 18

2. 经典继承的问题

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

组合继承

综合了原型链和经典继承:使用原型链继承原型上的属性和方法,用盗用构造函数继承实例属性。

这样即可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。

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() {
  console.log(this.age)
}

let instance1 = new SubType('Nicholas', 20)
instance1.colors.push('black')
console.log(instance1.colors) // ["red", "green", "blue", "black"]
instance1.sayName() // Nicholas
instance1.sayAge() // 20

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

组合继承弥补了原型链和经典继承的不足,是 JavaScript 中使用最多的继承模式。而且组合继承也保留了 instanceof 操作服和 isPrototypeOf() 方法识别合成对象的能力

原型式继承

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

object 方法本质上是对传入对象执行了一次浅复制

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

let person1 = object(person)
person1.name = 'Greg'
person1.friends.push('Rob')

let person2 = object(person)
person2.name = 'Linda'
person2.friends.push('Barbie')

console.log(person.friends) // ["Shelby", "Court", "Van", "Rob", "Barbie"]
ECMAScript 5 增加了 Object.create() 方法将原型式继承的概念规范化了
let person = {
  name: 'Nicholas',
  friends: ['Shelby', 'Court', 'Van']
}

let person1 = Object.create(person, {
  name: {
    value: 'Greg'
  }
})

console.log(person1.name) // Greg

注意:属性中包含的引用值始终会在相关对象间共享,和使用原型模式是一样的

寄生式继承

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

function createAnother(original) {
  // let clone = object(original)
  let clone = Object.create(original)
  clone.sayHi = function() {
    console.log('hi')
  }
  return clone
}

object() 函数不是寄生式继承所必需的,任何返回新对象的函数都可以在这里使用

注意:通过寄生式继承给对象添加函数会导致函数难以重用

寄生式组合继承

组合继承存在效率问题:父类构造函数始终会被调用两次;一次是在创建子类原型时调用,另一次是在子类型构造函数中调用。

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) // 第二次调用 SuperType()

  this.age = age
}

SubType.prototype = new SuperType() // 第一次调用 SuperType()
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function() {
  console.log(this.age)
}

寄生式组合继承通过盗用构造函数继承属性,使用混合式原型链继承方法

function inheritPrototype(subType, superType) {
  let prototype = Object.create(superType.prototype)
  prototype.constructor = subType
  subType.prototype = prototype
}

inheritPrototype() 函数实现了寄生式继承的核心逻辑。

这个函数接收两个参数:子类构造函数和父类构造函数;

第一步创建父类原型的一个副本

给返回的 prototype 对象设置 constructor 属性,解决由于重写原型导致默认 constructor 丢失问题

最后将新创建的对象赋值给子类型的原型

改下上面组合继承:

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
}

inheritPrototype(SubType, SuperType)

SubType.prototype.sayAge = function() {
  console.log(this.age)
}

使用 ECMAScript 5 的特性来模拟类似于类(class-like)的行为都有自己的问题

ECMAScript 6 引入 class 关键字具有正式定义类的能力

类(class)是 ECMAScript 中新的基础性语法糖,表面上看起来可以支持正式的类继承,但实际上它背后使用的仍然是原型和构造函数的概念

类定义

类声明和类表达式

// 类声明
class Animal {}

// 类表达式
const Animal = class {}

与函数的不同:

  • 类定义不能提升
  • 函数受函数作用域限制,类受块作用域限制

类的构成

  • 构造函数方法
  • 实例方法
  • 获取函数
  • 设置函数
  • 静态类方法

这些都不是必须的;类定义中的代码都在严格模式下执行

// 空类定义
class Foo {}

// 有构造函数
class Bar {
  constructor() {}
}

// 有获取函数
class Baz {
  get myBaz() {}
}

// 有静态方法
class Qux {
  static myQux() {}
}

类表达式的名称是可选的

可以通过 name 属性取得类表达式的名称字符串

不能在类表达式作用域外部访问这个标识符

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

let p = new Person()

p.identify() // PersonName PersonName

console.log(Person.name) // PersonName
console.log(PersonName) // Uncaught ReferenceError: PersonName is not defined

类构造函数

constructor 关键字表示类的构造函数;会告诉解释器在使用 new 操作符创建类的新实例时,应该调用这个函数

1. 实例化

使用 new 调用类的构造函数会执行如下操作:

  1. 在内存中创建一个新对象
  2. 这个新对象内部的 [[Prototype]] 特性被赋值为构造函数的 prototype 属性
  3. 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)
  4. 执行构造函数内部的代码(给新对象添加属性)
  5. 如果构造函数返回非空对象,则返回这个对象,否则返回之前创建的新对象

与构造函数的区别:

调用类构造函数必须使用 new 操作符;普通构造函数如果不使用 new 调用,那么会以全局的 this 作为内部对象;调用类构造函数没有使用 new 则会抛出错误

实例、原型和类成员

1. 实例成员

添加到新创建实例(this)上的“自有”属性

每个实例都对应一个唯一的成员对象

2. 原型方法与访问器

类块中定义的方法

class Person {
  constructor() {
    // 添加到 this 的所有内容都会存在于不同的实例上
    this.locate = () => console.log('instance')
  }

  locate() {
    // 定义在类的原型对象上
    console.log('prototype')
  }
}

可以把方法定义在类构造函数中或者类块中,原始值或对象不可以

类定义支持获取和设置访问器

class Person {
  set name(newName) {
    this._name = newName
  }

  get name() {
    return this._name
  }
}

3. 静态类方法

使用 static 关键字

静态成员每个类只能有一个

在静态成员中,this 引用类自身

class Person {
  constructor() {
    // 添加到 this 的所有内容都会存在于不同的实例上
    this.locate = () => console.log('instance')
  }

  locate() {
    // 定义在类的原型对象上
    console.log('prototype')
  }

  // 定义在类本身上
  static locate() {
    console.log('class', this)
  }
}

静态类方法非常适合作为实例工厂:

class Person {
  constructor(name) {
    this._name = name
  }

  sayName() {
    console.log(this._name)
  }

  static create() {
    return new Person('Andy')
  }
}

console.log(Person.create())

4. 非函数原型和类成员

类定义不支持在原型或类上添加成员数据,但在类定义外部,可以手动添加

类定义中之所以没有显示支持添加数据成员,是因为在共享目标(原型和类)上添加可变(可修改)数据成员是一种反模式。一般来说,对象实例应该独自拥有通过 this 引用的数据。

5. 迭代器与发生器方法

类定义语法支持在原型和类本身上定义生成器方法

继承

1. 基础继承

使用 extends 关键字,就可以继承任何拥有 [[Construct]] 和原型的对象

class Vehicle {
  identifyPrototype(id) {
    console.log(id, this)
  }

  static identifyClass(id) {
    console.log(id, this)
  }
}

class Bus extends Vehicle {}

let v = new Vehicle()
let b = new Bus()

b.identifyPrototype('bus') // bus Bus {}
v.identifyPrototype('vehicle') // vehicle Vehicle {}

Bus.identifyClass('bus') // bus class Bus extends Vehicle {}
Vehicle.identifyClass('vehicle') // vehicle class Vehicle {}

extends 关键字也支持类表达式 let Bus = class extends Vehicle {}

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

super()

派生类的方法可以通过 super 关键字引用它们的原型。

在类构造函数中使用 super 可以调用父类构造函数

class Vehicle {
  constructor() {
    this.hasEngine = true
  }
}

class Bus extends Vehicle {
  constructor() {
    // 不要在调用 super() 之前引用 this,否则会抛出 ReferenceError

    super() // 相当于 super.constructor()

    console.log(this instanceof Vehicle) // true
    console.log(this) // Bus { hasEngine: true }
  }
}

new Bus()

在静态方法中可以通过 super 调用继承的类上定义的静态方法:

class Vehicle {
  static identify() {
    console.log('vehicle')
  }
}

class Bus extends Vehicle {
  static identify() {
    super.identify()
  }
}

Bus.identify() // vehicle
ES6 给类构造函数和静态方法添加了内部特性 [[HomeObject]] ,这个特性是一个指针,指向定义该方法的对象。这个指针是自动赋值的,而且只能在 JavaScript 引擎内部访问。 super 始终会定义为 [[HomeObject]] 的原型

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

  • super 只能在派生类的构造函数和静态方法中使用
class Vehicle {
  constructor() {
    super() // Uncaught SyntaxError: 'super' keyword unexpected here
  }
}
  • 不能单独应用 super 关键字
class Vehicle {}

class Bus extends Vehicle {
  constructor() {
    console.log(super) // Uncaught SyntaxError: 'super' keyword unexpected here
  }
}
  • 调用 super 会调用父类构造函数,并将返回的实例赋值给 this
class Vehicle {}

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

    console.log(this instanceof Vehicle)
  }
}

new Bus() // true
  • super() 的行为如同调用构造函数,如果需要给父类构造函数传参,需手动传入
class Vehicle {
  constructor(licensePlate) {
    this.licensePlate = licensePlate
  }
}

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

console.log(new Bus('A2333')) // Bus {licensePlate: "A2333"}
  • 如果没有定义类构造函数,在实例化派生类时会调用 super() ,而且会传入所有传给派生类的参数
class Vehicle {
  constructor(licensePlate) {
    this.licensePlate = licensePlate
  }
}

class Bus extends Vehicle {}

console.log(new Bus('A2333')) // Bus {licensePlate: "A2333"}
  • 在类构造函数中,不能在调用 super() 之前引用 this
class Vehicle {}

class Bus extends Vehicle {
  constructor() {
    console.log(this)
  }
}

new Bus() // Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
  • 如果在派生类中显示定义类构造函数,则要么必须在其中调用 super() ,要么必须在其中返回一个对象
class Vehicle {}

class Car extends Vehicle {}

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

class Van extends Vehicle {
  constructor() {
    return {}
  }
}

console.log(new Car()) // Car {}
console.log(new Bus()) // Bus {}
console.log(new Van()) // {}

3. 抽象基类

ECMAScript 没有专门支持抽象基类的语法,可以通过实例化时检测 new.target (保存通过 new 关键字调用的类或函数)是不是抽象基类,可以阻止对抽象基类的实例化:

class Vehicle {
  constructor() {
    console.log(new.target)
    if(new.target === Vehicle) {
      throw new Error('Vehicle cannot be directly instantiated')
    }
  }
}

class Bus extends Vehicle {}

new Bus() // class Bus extends Vehicle {}
new Vehicle() // class Vehicle()
// Uncaught Error: Vehicle cannot be directly instantiated
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值