3

JS面向对象之四 【new】 (创建特定对象的语法糖)

个人学习笔记分享

为了讲清楚面向对象,实际上是需要几个前置知识的。
包括前面的几篇文章【原型链】 【this】 和今天要说的【new】

还是先说结论: new只是一个语法糖,这个语法糖被设计出来,使用场景是批量创建对象

从场景说起: 假设这个世界没有new,我们如何批量创建一百个士兵obj?

通篇文章,我们都是在实现这个需求,不断优化,最终实现一个new语法糖。

类似这样的士兵ojb

var 士兵 = {
    ID: 1,
    兵种: '美国大兵',
    攻击力: 5,
    生命值: 42,
    攻击: function(){console.log('攻击')},
    防御: function(){console.log('防御')},
    死亡: function(){console.log('死亡')},
}

版本v1.0: 最直觉的方法当然是循环100次

var 士兵
var 士兵们 = []
for (var i = 0 ; i < 100 ; i++){
     士兵 = {
        ID: 1,
        兵种: '美国大兵',
        攻击力: 5,
        生命值: 42,
        攻击: function(){console.log('攻击')},
        防御: function(){console.log('防御')},
        死亡: function(){console.log('死亡')},
    }
    士兵们.push(士兵)
}

最终我们得到了array士兵们

根据第二篇文章 JS面向对象之二【原型链】(对象和对象的关系).md),我们知道这里的攻击、防御、死亡都是匿名函数,在内存空间都会占据空间。
这些函数的内容完全相同,300个匿名函数却要占用300个内存空间,十分浪费。

自然地,我们就想到JS的原型链就是专门解决这个问题的,我们把共同的属性放到__proto__里,所有士兵的__proto__都指向相同的内存空间。

版本v1.1: 优化内存空间,使用__proto__保存共同的属性

var 士兵
var 士兵们 = []

for (var i=0 ; i<100 ; i++){
    士兵 = {
        ID: 1,
        生命值: 42
    }
   士兵.__proto__ = 士兵共
   士兵们.push(士兵)
}

var 士兵共 = {
    兵种: '美国大兵',     // 如果兵种和攻击力也是一样的话,那么也需要放到__proto__里去共享
    攻击力: 5,
    攻击: function(){console.log('攻击')},
    防御: function(){console.log('防御')},
    死亡: function(){console.log('死亡')},
}

最终依旧得到了array士兵们

(以上代码可粘贴运行)
现在我们省掉了之前297个匿名函数所占用的空间。让他们的__proto__都指向'士兵共'这个内容空间就OK了

版本v1.2: 变量名称优化成英文

var soldier
var soldiers= []

for (var i=0 ; i<100 ; i++){
    soldier = {
        ID: 1,
        生命值: 42
    }
   soldier.__proto__ = soldierCommon
   soldiers.push(soldier)
}

var soldierCommon = {
    兵种: '美国大兵',     // 如果兵种和攻击力也是一样的话,那么也需要放到__proto__里去共享
    攻击力: 5,
    攻击: function(){console.log('攻击')},
    防御: function(){console.log('防御')},
    死亡: function(){console.log('死亡')},
}

版本v2.0: 封装成createSoldier函数,避免意大利式面条,让代码更加集中。


    var soldierCommon = {
        兵种: '美国大兵',     // 如果兵种和攻击力也是一样的话,那么也需要放到__proto__里去共享
        攻击力: 5,
        攻击: function(){console.log('攻击')},
        防御: function(){console.log('防御')},
        死亡: function(){console.log('死亡')},
    }
    
    function createSoldier(){
        var obj = {
            ID: 1,
            生命值: 42
        }
        obj.__proto__ = soldierCommon
        return obj
    }
    
------分割线以上是创建构造函数, 分割线以下,是使用构造函数------

    var soldiers = []
    for(var i = 0 ;i<100 ; i++){
        soldiers.push(createSoldier()) 
    }

版本v2.1: 让开发者一眼就知道soldierCommon和createSoldier是有关系的,让代码进一步集中


    // 让soldierCommon成为构造函数的属性(因为函数也是obj,当然可以有属性)
    // soldierCommon可以改叫xxx,只是一个名字,只是为了让这个对象和createSoldier产生联系
    
    createSoldier.xxx = {
        兵种: '美国大兵',     
        攻击力: 5,
        攻击: function(){console.log('攻击')},
        防御: function(){console.log('防御')},
        死亡: function(){console.log('死亡')},
    }
    
    function createSoldier(){
        var obj = {
            ID: 1,
            生命值: 42
        }
        obj.__proto__ = createSoldier.xxx
        return obj
    }
    
------分割线以上是创建构造函数, 分割线以下,是使用构造函数------

    var soldiers = []
    for(var i = 0 ;i<100 ; i++){
        soldiers.push(createSoldier()) 
    }

那么现在还可以优化吗? 上面这段代码,唯一的缺点就是xxx不太容易理解。

所以,JS之父为了容易理解将这个xxx,也就是共有属性的引用,统一叫做prototype,虽然prototype对于中国人来说依旧不好理解。

版本v2.2: 完美的代码,将xxx改为prototype (只是一个名字,但是JS之父将它命名成了prototype)

    
    createSoldier.prototype = {
        兵种: '美国大兵',     
        攻击力: 5,
        攻击: function(){console.log('攻击')},
        防御: function(){console.log('防御')},
        死亡: function(){console.log('死亡')},
    }
    
    function createSoldier(){
        var obj = {
            ID: 1,
            生命值: 42
        }
        obj.__proto__ = createSoldier.prototype
        return obj
    }
    
------分割线以上是创建构造函数, 分割线以下,是使用构造函数------

    var soldiers = []
    for(var i = 0 ;i<100 ; i++){
        soldiers.push(createSoldier()) 
    }

好了,仔细看看上面的代码,还有什么可优化的地方吗?

内存、变量名、代码集中都做了优化,也就是范例式的代码。

那么,JS之父也认为这个就是最好的代码,他希望,当JS开发者们需要批量创建特定对象时,都这样写。

于是,JS之父创造了new这个语法糖。

版本v3.0 让我们看看JS之父创造的new语法糖

 function Soldier(){
     this.ID  = 1
     this.生命值 = 42
 }
 
 Soldier.prototype.兵种 = '美国大兵'
 Soldier.prototype.攻击力 = '65
 Soldier.prototype.攻击 = function(){console.log('攻击')}
 Soldier.prototype.防御 = function(){console.log('防御')}
 Soldier.prototype.死亡 = function(){console.log('死亡')}
 
 ------分割线以上是创建构造函数, 分割线以下,是使用构造函数------
 
 var soldiers = []
 for(var i = 0 ;i<100 ; i++){
     soldiers.push( new Soldier()) 
 }

对比v2.2 和 v3.0 这两个版本,就是new帮我们做的事情。

v2.2 没有使用new语法的函数createSoldier
    function createSoldier(){
        var obj = {
            ID: 1,
            生命值: 42
        }
        obj.__proto__ = createSoldier.prototype
        return obj
    }
v3.0 使用了new语法的函数Soldier
     function Soldier(){
     //   this = {}                                          JS偷偷做的第1个事情                               
     //   this.__proto__ = Soldier.prototype                 JS偷偷做的第2个事情 
     
        this.ID  = 1
        this.生命值 = 42
         
     // return this                                          JS偷偷做的第3个事情 
         
     }
     
     Soldier.prototype = {                                  JS偷偷给Soldier.prototype加了个constructor属性
        constructor : Soldier
     }
对比一下,当你使用new语法,JS之父帮你做了什么事情
  • 第一 , JS帮你在函数里创建了一个this空对象,并且这个this指向是构造函数的实例
  • 第二 , JS帮你让this的__proto__ 指向 构造函数的原型,也就是prototype
  • 第三 , JS帮你偷偷return了 this
  • 第四 , JS偷偷给Soldier.prototype加了个constructor属性
其他注意事项
  • 构造函数首字母大写,习惯写法
  • __proto__不能在生产环境出现,因为会严重影响性能
  • 用new调用的时候,也没法用call了,因为JS之父帮你call了,this的指向就像你见到的指向实例

所以使用了new,就可以让代码没有废话。

你只需要设置对象的自有属性和共有属性。
JS帮你创建空对象,帮你搞定this指向,帮你改变this的__proto__,帮你return this


Ziwei
4.1k 声望420 粉丝

前端程序员