最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 对象、类与面向对象编程

    正文概述 掘金(lyn-ho)   2020-12-06   376

    认识对象

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

    我们用状态来描述对象

    状态的改变即是行为

    面向对象三大特性

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

    Class vs Prototype

    Class(类)

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

    Prototype(原型)

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

    JavaScript 对象

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

    可以把 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

    访问器属性

    访问器属性有 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
        }
    	}
    })
    

    定义多个属性

    接收两个参数:

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

    读取属性的特性

    接收两个参数:

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

    返回值:

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

    接收一个参数:

    • obj :任意对象

    返回值:

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

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

    Object 的一些扩展

    Object.assign

    参数:

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

    原型模式

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

    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
    

    对象、类与面向对象编程

    虽然不是所有实现都对外暴露了 [[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 文档中指出:

    为了避免使用 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
    

    对象、类与面向对象编程

    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
    

    对象、类与面向对象编程

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

    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
    }
    

    继承

    原型链

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

    • 每个构造函数都有一个原型对象,原型有一个属性指回构造函数

    • 实例有一个内部指针指向原型

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

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

    ?:

    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
    

    对象、类与面向对象编程

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

    1. 默认原型

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

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

    对象、类与面向对象编程

    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"]
    
    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. 非函数原型和类成员

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

    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
    

    在使用 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
    

    起源地下载网 » 对象、类与面向对象编程

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元