关于object:JavaScript的对象原型类和继承

5次阅读

共计 5302 个字符,预计需要花费 14 分钟才能阅读完成。

前言

HTML万物皆标签。

CSS万物皆盒。

JavaScript万物皆对象。

对象

JavaScript对象的实质是 数据和性能的汇合 ,语法上体现为 键值对的汇合

对象的键能够了解为变量名。

对象的值的类型能够是任意数据类型。

键值对

键和值之间用 : 相连。

多组键值对之间用 , 宰割。

let profile = {
    name: '吴彦祖',
    age: 48,
    charmingThenMe: false,
    works: ['特警新人类', '新警察故事', '门徒', '除暴'],
    bio: function () {alert('你很能打吗?你会打有个屁用啊。')
    },
    hi() {this.bio()
        alert('进去混要有权势,要有背景,你哪个道上的?')
    }
}

依照 值是否为函数 这一规范,进一步将键值对分为 属性(property) 办法(method)

对象为数据和性能的汇合,数据对应属性,性能对应办法。

profile 为例,前四个为属性,后俩为办法。

hi() {...}

// 等价于

hi: function() {...}

// 下面的写法是办法的短语法。

拜访

有两种形式拜访对象的键值对,别离为 点式拜访 括号式拜访

// 点式拜访
profile.name // '吴彦祖'

// 括号式拜访
profile['age'] // 48

当你发现在一些非凡场景下应用点式拜访无奈实现时,记得尝试括号式拜访。

这些非凡场景大多呈现于键产生于运行时。

比方:
当你在遍历中须要从实参中获取键。
或者你须要同时定义对象的键和值。

构造函数

理论开发中,若依照下面的形式应用对象,意味着每须要一个 profile 都须要手动写出一个领有雷同键的对象,这会带来灾难性的结果:

  • 巨量的反复代码
  • 一旦须要更新属性或办法,则必须遍历每一个对象

咱们须要形象

具体来说,咱们须要一个函数,能够主动创立具备雷同键的对象,而不是每次应用时,手动重写一遍键。

// 键只须要在定义 createProfile()时写一次
function createProfile(name, age, charmingThenMe, works, bio, hi) {let o = {}
    o.name = name
    o.age = age
    o.charmingThenMe = charmingThenMe
    o.works = works
    o.bio = function () {alert(bio)
    }
    o.hi = function () {o.bio()
        alert(hi)
    }
    return o
}
// 后续生成对象时,只须要写值,键会主动填充
let edisonChen = createProfile(
    '陈冠希',
    42,
    false,
    ['无间道', '头文字 D', '神枪手'],
    '在吗拓海',
    '微信转账三百块'
)

edisonChen.name
edisonChen.hi()

形象实现。

createProfile() 仿佛有点冗余,咱们剖析一下 createProfile() 外部都干了些什么:

  1. let o = {} 创立一个空对象
  2. o.foo = bar 为对象增加属性和办法
  3. return o 返回新创建的对象

这不就是 new 的作用吗!

当应用 new 调用一个函数(此处称之为f())时,具体会有以下过程:

  1. 创立一个空对象o
  2. o 的原型指向 f() 的 prototype 属性
  3. this 绑定到o,并执行f()
  4. 返回o

这意味着,如果咱们应用 new 来调用生成对象的函数,咱们只须要关注外围的业务逻辑即可,诸如生成空对象、绑定this、返回对象这种杂活儿间接委托进来。

function Profile(name, age, charmingThenMe, works, bio, hi) {
    this.name = name
    this.age = age
    this.charmingThenMe = charmingThenMe
    this.works = works
    this.bio = function () {alert(bio)
    }
    this.hi = function () {this.bio()
        alert(hi)
    }
}

Profile函数体内只有新对象所需的数据和办法,咱们真正关注的也只是这一部分。

至于函数名为什么从 createProfile 变成了Profile,这齐全是按照常规的约定俗成:

应用对象名并以大写结尾作为该对象构造函数的名称

let j = new Profile('周杰伦', 43, false, ['夜曲', '最平凡的作品'], '喔唷', '不错哦')

j.works
j.hi()

这就是 JavaScript 的构造函数。

原型

JavaScript中的每个对象都有一个叫做原型的内建属性。

原型也是一个对象,原型也有原型,逐级溯源,造成 原型链

当一个对象的原型为 null 时,原型链完结。

一个对象,不仅能拜访本人独有的属性和办法,还能够拜访整个原型链上所有对象的属性和办法。

这解释了,为什么你只是申明了一个字符串,就能够调用一批字符串的内建办法。

// 在控制台中执行以下代码:let o = {
  name: 'a',
  hi() {console.log(`hello world`);
  }
}

o;

点开控制台返回的对象,你会发现,除了刚刚自定义的属性 name 和办法hi(),还有一个长得很奇怪的 [[Prototype]],点开它你会发现另一个奇怪的键————__proto__

这就是对象 o 的原型,它不仅长得奇怪,甚至连名字都没有。

是的,ECMAScript认为对象原型“不配领有姓名”,只管你能够通过 o.__proto__ 拜访到它,但 o.__proto__ 是不受规范认可的属性,它只是各大浏览器外部的实现,并且曾经被官网废除。

获取原型

不要通过 __proto__ 属性去获取对象的原型。

应用 Object.getPrototypeOf() 获取。

let n = 123

do {n = Object.getPrototypeOf(n)
    console.log(n)
} while (n)

// Number
// Object
// null

设置原型

JavaScript中个别应用 Object.create() 或者 constructors 构造函数设置原型。

Object.create()

应用实参作为原型生成一个新对象。

let a = {hi() {console.log('hello world')
    }
}

let b = Object.create(a)

b.hi() // hello world

constructor

JavaScript中,所有函数都有一个叫 prototype 的属性,当应用 new 关键字来调用一个构造函数来生成新对象时,构造函数的这个 prototype 属性被设置为新生成对象的原型。

这个机制可能保障:只有指定了构造函数的 prototype 属性,所有由构造函数生成的新对象的原型都能保持一致。

// 申明并初始化一个 fruits 对象,作为原型对象供构造函数应用
let fruits = {hi() {console.log(` 吃个 ${this.name}${this.name}`)
    }
}

// 申明一个 Peach 构造函数
function Peach(name) {this.name = name}

// 设置构造函数的 prototype 属性
Peach.prototype.hi = fruits.hi
// 或者
// Object.assign(Peach.prototype, fruits)

let p = new Peach('桃') // 生成新对象 p

p.hi() // '吃个桃桃'

console.log(Peach.prototype === Object.getPrototypeOf(p)) // true
// 均为 fruits
  1. 应用字面量形式创立 fruits 对象,对象中定义了 hi 办法
  2. 申明构造函数 Apple,通过thisname属性和值增加到执行时产生的新对象上
  3. fruitshi办法增加到构造函数函数的 prototype 上(实际中原型往往具备多个属性,此时应用 Object.assign() 办法一次性增加会更高效)
  4. 应用 new 关键字生成新对象p
  5. 调用 phi办法(p自身并没有 hi 办法,而是继承自fruits
  6. 验证构造函数的 prototype 与新对象 p 的原型的一致性

自有属性

能够看到,下面的示例中,由构造函数 Peach 生成的对象 p 具备两个键:

  1. 一个是属性name,定义在构造函数中
  2. 一个是办法hi,定义在原型中

那些 间接定义在对象上 ,而非通过继承取得的属性,属于 自有属性

通过 Object.hasOwn() 判断属性是否为自有属性:

console.log(Object.hasOwn(p, 'name')) // ture

console.log(Object.hasOwn(p, 'hi')) // false

console.log(Object.hasOwn(fruits, 'hi')) // true

严格来说,自有属性应该被称之为自有键,如果你肯定要应用属性和办法来辨别键的话。
但属性在很多语境下是不辨别广义的属性和办法的,后者在规范中也未被定义。

原型小结

回顾下面的 fruits 示例,思考上面这个问题:

为什么要将办法定义在原型中,而将属性定义构造函数中呢?

因为这种行为与数据拆散的机制恰好符合了类和实例。

对象间因具备雷同的行为而被形象为类,行为(办法)被 类(原型)定义。

对象间因数据的差别而成为一个又一个的实例,数据(属性)被构造函数(返回实例)定义。

原型是 JavaScript 弱小而灵便的个性之一,它使得 代码复用 对象组合 成为可能。

JavaScript提供了一种更加开发者敌对的形式来实现类和实例 —— class

fruits 为例:

class Fruits {
    name

    constructor(name) {this.name = name}

    hi() {console.log(` 吃个 ${this.name}${this.name}`)
    }
}

let p = new Fruits('🍑')

p.hi() // 吃个🍑🍑

能够看出,class通过封装:

  • 申明并初始化原型对象
  • 申明构造函数
  • 初始化构造函数的 prototype 属性

等步骤,将基于原型链生成对象的语法,相较于纯构造函数而言,进一步简化。

省略属性

你甚至能够省略属性的申明。

class Fruits {constructor(name) {this.name = name}

    hi() {console.log(` 吃个 ${this.name}${this.name}`)
    }
}

let p = new Fruits('🍑')

p.hi() // 吃个🍑🍑

⚠️留神:实际中不要省略,因为这会 升高代码的可读性

属性的默认值

属性在初始化的时候,能够指定默认值。

class Fruits {
    name

    constructor(name) {this.name = name || '🍉'}

    hi() {console.log(` 吃个 ${this.name}${this.name}`)
    }
}

let w = new Fruits()

w.hi() // 吃个🍉🍉

省略构造函数

如果没有初始化的需要,则能够省略构造函数。

class Fruits {hi() {console.log(` 吃个屁 `)
    }
}

let p = new Fruits()

p.hi() // 吃个屁

继承

原型有原型链,类有继承。

以汽车举例,先定义汽车父类:

class Vehicle {
    brand // 所有的车都有品牌

    constructor(b) {this.brand = b}

    // 所有的品牌都有标语
    slogan() {console.log(`This is ${this.brand}`)
    }
}

通过继承汽车,定义电动汽车:

class EV extends Vehicle{
    batteryType // 电池类型
    remaining // 残余电量

    constructor(b, bt, r) {super(b)
        this.batteryType = bt
        this.remaining = r
    }

    charge() {
        let t = 0
        switch (this.batteryType) {
            case '三元锂':
                t = (1-this.remaining) / 2
                break
            case '磷酸铁锂':
                t = 1- this.remaining
                break
            default:
                t = Math.random()}
        console.log(` 尊贵的 ${this.brand}车主:` +
            ` 您的 ${this.batteryType || ''}电池 ` +
            ` 只需 ${Math.ceil(t*60)}分钟即可充斥!`
        )
    }
}

应用 extends 从父类继承属性和办法,应用 super() 调用父类的办法。

let t = new EV('Tesla', '三元锂', 0.3)
t.charge() // 尊贵的 Tesla 车主:您的三元锂电池只需 21 分钟即可充斥!let w = new EV('WuLing', '磷酸铁锂', 0.5)
w.charge() // 尊贵的 WuLing 车主:您的磷酸铁锂电池只需 30 分钟即可充斥!

通过继承,子类能够应用父类的属性和办法。

也能够定义子类本人的属性和办法。

甚至,父类中已有的属性和办法,也反对在子类中从新定义。

结语

尽管 class 看起来是个新货色,但实质上它还是原型链,或者说,它是原型链的语法糖。

JavaScript实质上不是传统意义上面向对象的编程语言,JavaScript是基于原型的编程语言。

正文完
 0