JavaScript中的构造函数

导言:首先,我想从ES5出发,探讨在创建对象时我们会面临哪些问题。接着,我们将深入了解构造函数是如何解决这些问题的。最后,我们将探究ES6对构造函数进行了哪些重要的升级与改进。

        一、创建对象遇到的问题

                1.1 获取自身属性繁琐

                

        在JavaScript中,我们要描述一个事物,最好的方式是创建一个对象,然后给这个对象添加属性和方法。
        比如,我们要描述一台手机:

var phone = {
      brand: '小米',
      model: 'MIX 3',
      fullName: '小米MIX 3'
    };

上面描述了一个手机的基本信息,有品牌、型号和全称。

我们会发现全称这个属性是重复的,我们可不可以把它提取出来呢?

像这样:

var phone = {
      brand: '小米',
      model: 'MIX 3',
      fullName: brand + ' ' + model
    };

这样当然是不可以的,因为这样写就相当于把brand和model当作一个变量了,我们把他们定义在phone里了,而我们在外部可没有定义。

那这样呢?

var phone = {
      brand: '小米',
      model: 'MIX 3',
      fullName: phone.brand + ' ' + phone.model
    };

当然也是不可以。你疯了吗?这个时候phone对象还没有创建呢。

那这样总行了吧?

var phone = {
      brand: '小米',
      model: 'MIX 3',
      getFullName(){
        return phone.brand + ' ' + phone.model
      }
    };

getFullName可是在phone对象创建之后运行的。运行一下,果然可以。

1.2 批量创建对象繁琐

如果我们要创建多个手机对象,可能需要多次复制粘贴,而且我们会发现,有很多代码逻辑是相同的。

        

 

那么有什么东西可以帮我们做这些重复工作呢?聪明的你马上就想到了方法,函数就是一段可重复执行的代码块,正好可以帮助我们做这些重复操作。于是你写下了如下方法:

    function CreatePhone (brand, model) {
      var phone = {}
      phone.brand = brand
      phone.model = model
      phone.fullName = brand + model
      return phone
    }
    var myPhone = CreatePhone('iPhone', '12')
    console.log(myPhone) // {brand: "iPhone", model: "12", fullName: "iPhone 12"}

    var myPhone2 = CreatePhone('小米', '15')
    console.log(myPhone2) // {brand: "小米", model: "15", fullName: "小米 15"}

完美。

后来,你看到了ES5 中的构造函数,你发现原来构造函数就是用来做这个的。

二、ES5中的构造函数

        你查阅了ES5文档,发现它可以轻松实现我们上面的需求,于是你写下了如下代码:

        你对之前的函数做了一些简单修改:

  1. 构造函数首字母大写
  2. 无需手动创建对象和返回对象
  3. 可通过this访问自身属性
  4. 需要使用new来调用
    function CreatePhone (brand, model) {
      this.brand = brand
      this.model = model
      this.fullName = this.brand + this.model
    }
    var myPhone = new CreatePhone('iPhone', '12')
    console.log(myPhone) // {brand: "iPhone", model: "12", fullName: "iPhone 12"}

    var myPhone2 = new CreatePhone('小米', '15')
    console.log(myPhone2) // {brand: "小米", model: "15", fullName: "小米 15"}

你发现创建对象如此简单。

2.1 对象的方法呢?

你不满足于你的手机只能描述名称,于是你为你的手机创建了一个打电话的方法:

    function CreatePhone (brand, model) {
      this.brand = brand
      this.model = model
      this.fullName = this.brand + ' ' + this.model
      
      this.call = function () {
        console.log('我的型号是'+ this.fullName +',我在打电话')
      }
    }
    var myPhone = new CreatePhone('iPhone', '12')
    myPhone.call() // 我的型号是iPhone 12,我在打电话

    var myPhone2 = new CreatePhone('小米', '15')
    myPhone2.call() // 我的型号是小米 15,我在打电话

你轻松地为手机创建了打电话方法。

但是,你走在路上,眉头一皱,发现事情没有这么简单。

你打印对比了这两个对象的打电话方法,发现竟然返回false,也就是说,他们分别开辟了一个内存空间。

    function CreatePhone (brand, model) {
      this.brand = brand
      this.model = model
      this.fullName = this.brand + ' ' + this.model

      this.call = function () {
        console.log('我的型号是'+ this.fullName +',我在打电话')
      }
    }
    var myPhone = new CreatePhone('iPhone', '12')

    var myPhone2 = new CreatePhone('小米', '15')

    console.log(myPhone.call === myPhone2.call); // false

你百思不解,为什么一样的方法,开辟了两个内存空间,如果有一百个方法,创建一百个对象,那得多浪费空间啊。

求知欲驱使你继续研究,你发现了原型链。

2.2 原型链

原型链

你读完原型链的文档,你发现了构造函数和实例对象之间的关系,并画出了一个最简单的三角关系。

    function CreatePhone (brand, model) {
      this.brand = brand
      this.model = model
      this.fullName = this.brand + ' ' + this.model

      this.call = function () {
        console.log('我的型号是'+ this.fullName +',我在打电话')
      }
    }
    var myPhone = new CreatePhone('小米', '15')

    console.log(CreatePhone.prototype); // {}
    
    console.log(myPhone.__proto__); // {}

    console.log(myPhone.__proto__ === CreatePhone.prototype); // true

你发现CreatePhone上有一个隐藏的prototype属性,它指向一个空对象。

而用CreatePhone生成的myPhone实例对象,你发现它身上也有一个隐藏的__proto__属性。

并且你发现他们共享同一片内存空间。事情变的有意思起来了。

你在文档上抓住了几个重点:

1、当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾;

2、JavaScript 只有一种结构:对象。每个对象(object)都有一个私有属性指向另一个名为原型(prototype)的对象。原型对象也有一个自己的原型,层层向上直到一个对象的原型为 null

 

也就是说,我们在实例对象上访问一个属性或者方法,它会先看自己有没有,如果没有再看构造函数原型上面有没有。

    function CreatePhone (brand, model) {
      this.brand = brand
      this.model = model
      this.fullName = this.brand + ' ' + this.model
    }

    CreatePhone.prototype.call = function () {
      console.log('我的型号是' + this.fullName + ',我在打电话')
    }

    var myPhone = new CreatePhone('小米', '15')
    myPhone.call() // 我的型号是小米 15,我在打电话

    var yourPhone = new CreatePhone('华为', 'P50')
    yourPhone.call() // 我的型号是华为 P50,我在打电话

    console.log(myPhone.call === yourPhone.call); // true

上面,你发现终于每个实例的打电话方法都可以共享一块内存空间了。这不能再完美了。

2.3 ES5构造函数中的继承

你创建了一个手机构造函数后,你又想创建一个卫星手机的构造构造函数。

这个卫星手机也有商标、型号、全称的属性以及打电话的方法。并且还有一些属于自己的属性方法。

    function Phone (brand, model) {
      this.brand = brand
      this.model = model
      this.fullName = this.brand + ' ' + this.model
    }

    Phone.prototype.call = function () {
      console.log('我的型号是' + this.fullName + ',我在打电话')
    }


    function SatellitePhone (brand, model) {
      this.brand = brand
      this.model = model
      this.fullName = this.brand + ' ' + this.model
      this.antenna = '天线'
    }

    SatellitePhone.prototype.call = function () {
      console.log('我的型号是' + this.fullName + ',我在打电话')
    }

你复制了一份改了改,发现一样能用。

但是机智的你发现了一个问题。如果你想给所有手机增加一个color属性,你还得给Phone和SatellitePhone都增加一份,同时维护两份数据,这太累了。

 那你不能直接在SatellitePhone中调用一次Phone呢:

    function SatellitePhone (brand, model) {
      Phone(brand, model)
      this.antenna = '天线'
    }

显然this指向会出问题。

this可以用call解决,聪明的你写出了以下代码:

    function Phone (brand, model) {
      this.brand = brand
      this.model = model
      this.fullName = this.brand + ' ' + this.model
    }

    Phone.prototype.call = function () {
      console.log('我的型号是' + this.fullName + ',我在打电话')
    }

    function SatellitePhone (brand, model) {
      Phone.call(this, brand, model)
      this.antenna = '天线'
    }

    var myPhone = new SatellitePhone('小米', '10')
    console.log(myPhone); // {brand: '小米', model: '10', fullName: '小米 10', antenna: '天线'}

利用call方法修改this指向,这样很好地解决了属性继承的问题。

那么,方法怎么继承呢?

你冥思苦想,你鼠标向上一划,看到了原型链的知识。

你灵机一动,只要可以让SatellitePhone的prototype得到Phone上的属性和方法就可以了:

SatellitePhone.prototype = Phone.prototype

但是这样会导致 SatellitePhone 的实例和 Phone 的实例共享同一个原型对象。这可能会导致意外的副作用,如果在 Phone.prototype 上添加或修改属性时,会影响到所有继承自 Phone 的实例。

SatellitePhone.prototype = new Phone()
SatellitePhone.prototype.constructor = SatellitePhone

这里通过 new 关键字创建一个 Phone 实例,这个实例会拥有 Phone 构造函数中定义的所有属性和方法。将这个实例赋值给 SatellitePhone.prototype,使得 SatellitePhone 的实例可以直接访问这些属性和方法。

对象原型(__proto__)和构造函数(prototype)原型对象里面都有一个属性constructor属性,constructor我们称为构造函数,因为它指向构造函数本身。
        constructor主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。

因为直接赋值会覆盖掉SatellitePhone.prototype内的constructor,上面还重新指定了constructor。

ES5实现继承完整代码:
    function Phone (brand, model) {
      this.brand = brand
      this.model = model
      this.fullName = this.brand + ' ' + this.model
    }

    Phone.prototype.call = function () {
      console.log('我的型号是' + this.fullName + ',我在打电话')
    }

    function SatellitePhone (brand, model) {
      // 继承属性
      Phone.call(this, brand, model)
      this.antenna = '天线'
    }

    // 继承prototype方法
    SatellitePhone.prototype = new Phone()
    SatellitePhone.prototype.constructor = SatellitePhone

这种继承方式一般也被称为组合继承。组合继承就是将原型链和借用构造函数的技术结合到一起实现继承。

2.4 ES5 构造函数的问题

最后,你发现ES5构造函数的写法好像不太得劲,比如:

  • 为什么用构造函数定义一个类,它的属性和方法好像分离了一样(一个定义在大括号里,一个定义在外面的prototype上)
  • 继承写起来好麻烦
  • 用for...in去遍历实例对象,可以把prototype上的方法遍历出来

等等。于是,你决定去看看ES6有没有新的解决方案。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值