JavaScript之对象

作者: FoolishFlyFox | 来源:发表于2017-07-08 15:06 被阅读58次

    在ES6出现之前,JavaScript不能真正被称为 面向对象的编程语言,因为 class 仅仅作为其保留字而非关键字,而ES6之后,引入了class,使程序员可以用自己更加熟悉的方式创建对象;

    至于ES6和ES5有什么区别,应该就是上面提到的可以让程序员更爽地coding,而程序员爽了,根据 工作量守恒定律,总有某个事物要干更多的活,没错,就是计算机。

    为了兼容某些不支持ES6的浏览器,我们可以引入 Babel 库将ES6代码 “编译” 成ES5之后执行,而对于支持ES6的浏览器,在其JavaScript引擎中会自动进行“编译”操作;

    所以,总体上看,ES6和ES5在功能上是等效的,即用ES6能完成的任务,用ES5必然能够完成,只是在语法上,ES6提供了更多的 语法糖,让程序员尝到甜头;所以为了更好的理解JavaScript对象,我们回归初心,从ES5中窥视JavaScript所创建的对象世界;

    对象的创建

    对象有两个基本元素:属性和方法;

    属性用于存储数据,方法用于存储代码;接下来,我们从简单到复杂,来理解JavaScript创建对象的演变史。

    创建Object对象并赋值时代

    我们可以通过 new Object 创建Object,并为其赋属性和方法来创建对象:

    // 代码段 1
    var p = new Object();
    p.name = "x";
    p.age = 20;
    p.say = function(){
      console.log("My name is",this.name,",this year is",this.age);
    }
    

    这样我们就得到一个含有2个属性,1个方法的对象;

    通过执行 p.say() 得到输出 : My name is x ,this year is 20 ;

    但是,这些几行代码非常松散,每一行代码都像一条单独的语句,为了体现这些属性和方法是一个整体,而不是一个个独立的存在,我们需要进入下一个时代;

    字面量赋值时代

    我们通过给一个变量赋一个字面量,即可创建一个对象:

    // 代码段 2
    var p = {
      name : "x" ,
      age : 20 , 
      say : function(){
        console.log("My name is",this.name,",this year is",this.age);
      }
    }
    

    这样,我们就创建好了一个对象,这个对象有了自己的属性和方法;

    通过 console.log(typeof p) ,输出为object可知, p 的类型是一个对象;

    代码段1和代码段2相比,代码段2的结构更为合理,所有的属性和方法都用大括号包含,更利于阅读,也体现了整体性,但功能上是等效的;

    不过我们又发现,每次要创建一个相似的对象,都需要写一遍属性名,非常地不优雅,所以,我们又要进入下一个时代;

    工厂模式时代

    我们可以通过调用一个函数创建我们需要的对象,并返回该对象,从而使代码可重用,这个函数就是传说中的 工厂,代码如下:

    // 代码 3
    function createPerson(name,age){
      var t = new Object();
      t.name = name;
      t.age = age;
      t.say = function(){
        console.log("My name is",this.name,",this year is",this.age);
      }
      return t;
    }
    var p = createPerson("x",20);
    

    代码3是将代码1变为了工厂模式;

    下面再将代码2也变为工厂模式:

    function createPerson(name,age){
      return {
        name:name,
        age : age,
        say = function(){
          console.log("My name is",this.name,",this year is",this.age);
        }
      };
    }
    var p = createPerson("x",20);
    ```
    工厂模式的代码要比前面两个时代的代码优雅,我们只需要调用一个函数即可获得我们想要的对象;
    
    但是,我们又发现了一个新的问题(别问我为什么总是能发现新问题,因为就是有一双善于观察的眼睛,手动傲娇 ^_^):
    
    通过上述3中方式创建的对象在使用 `typeof` 时,返回的都是 `object`,而通过 `实例 instanceof 类` 只有在类为 `Object`时,才返回true,就是说,上面3中方法创建的对象都是无差别的对象,我们不能分辨出它们的类型;
    
    这就麻烦了,比如我们有这么一个函数:
    ```javascript
    function seeDoctor( o ){
      if(o是人){
        请人医治疗
      }
      if(o是动物){
        请兽医治疗
      }
    }
    ```
    那我们创建的对象因为不能判断其是人是兽,将不能选择适合的治疗方案;
    
    要解决这个问题,有两种思路:
    - 给对象添加信息,即为每一个对象添加一个属性 `type` ,用于指明其类型;
    - 让js解释器能够判断其类型;
    
    第一种方式比较 *丑陋*,我们需要管理更多的数据,但比较容易理解;第二种是更优雅的方法,也推动我们进入下一个时代;
    
    ### 构造函数时代
    
    构造函数时代的主要任务是让创建的对象自带类别说明属性,即通过`instanceof` 就能判断出其所属的类:
    ```javascript
    // 类是一个函数,约定:
    // 普通函数第一个字母小写,类函数第一个字母大写
    function Person(name,age){
      this.name = name;
      this.age = age;
      this.say = function(){
        console.log("My name is",this.name,",this year is",this.age);
      }
    }
    function Animal(){}
    var p = new Person("x",20) ;
    ```
    注意,创建对象时,必须使用关键字 **new** 。
    
    此时,我们通过 `p instanceof Person`,返回的结果为 `true`,而通过 `p instanceof Animal` ,返回结果为 `false`,从而使对象实例自带类型属性;
    
    完美! But,又双叒叕发现了不足,我们用上述各种函数创建两个对象:
    ```javascript
    var p1 = new Person("x",20);
    var p2 = new Person("y",21);
    console.log(p1.say==p2.say) ; // 输出的是false
    ```
    我们发现:虽然 *say* 函数的代码相同,但两个对象实例的 *say* 居然指向不同的代码块,如果我们有100个实例,相同的代码块就需要有100份,极大的内存浪费,这是我们所不能忍受的,因此迫切希望下一个时代的到来!
    
    ### 构造函数+原型时代
    
    原型就是所有对象实例所共享的一个 **对象**,这个对象中的属性就是共享属性(在c++中称为静态变量),方法就是共享方法; 
    
    ```javascript
    function Person(name,age){
      this.name = name;
      this.age = age;
    }
    Person.prototype.say = function(){
      console.log("My name is",this.name,",this year is",this.age);
    }
    var p = new Person("x",20);
    ```
    上述代码创建的对象实例 p 有自己的属性name和age,以及共享的方法say;通过 `p.say()` 即可打印出:*My name is x ,this year is 20* ;
    
    执行 `p.say()` 的时候,p先搜索其自身是否有方法 `say`,如果有,就执行,如果没有,就搜索其原型对象是否有 `say` 方法,如果还是没有,就搜索其原型对象的原型对象是否有say属性(即沿着原型链搜索say方法,这也是继承的实现机制),如果原型链上都没有say方法,就抛出错误,否则,执行搜索到的方法。
    
    p通过属性`p.__proto__` 指向原型对象 `Person.prototype` , 从而获取原型上的所有属性和方法;
    
    如果p上也定义一个方法 `say`:
    ```javascript
    p.say = function(){console.log("Hello,world");}
    ```
    则该方法将会 **覆盖** 原型上的say方法,即调用 `p.say` 输出的将是 *Hello,world* ,而如果通过 `delete p.say` 删除掉 `say` 属性,则调用 `p.say` 时,执行的代码又是原型上的 say 代码;
    
    总结:原型就是一个类所创建的所有对象实例共享的一个对象;
    
    但是,原型对象的定义和构造函数分开了,这又使结构不太优美,所以,我们又得进入下一个时代;
    
    ### 构造原型时代
    
    为了解决原型定义和构造函数分离的问题,我们决定将原型定义放到构造函数中,就出现了以下代码:
    
    ```javascript
    function Person(name,age){
      this.name = name;
      this.age = age;
      Person.prototype.say = function(){
        console.log("My name is",this.name,",this year is",this.age);
      }
    }
    var p = new Person("x",20);
    ```
    OK,完成了原型定义和构造函数的合并,结构也变得更加优美了,但是,又出现了一个问题:
    
    每次执行创建 Person 对象实例的时候,都要重新定义一遍 `Person.prototype.say` 方法,虽然这不会增加内存泄漏(以前定义的say代码由于没有被引用,内存块将会被自动回收),但却增加了cpu的工作量,所以我们需要进入下一个时代;
    
    ### 优化构造原型时代
    
    为了避免 `Person.prototype.say` 函数的重复定义,我们可以先判断该函数是否已定义,如果没有定义,再对其进行定义:
    
    ```javascript
    function Person(name,age){
      this.name = name;
      this.age = age;
      if(typeof(Person.prototype.say)=="undefined"){
        Person.prototype.say = function(){
          console.log("My name is",this.name,",this year is",this.age);
        } ;
      }
    }
    var p = new Person("x",20);
    ```
    通过以上7个时代的迭代,我们终于在 JavaScript中创建了一个基本上符合我们要求的对象; 
    
    完!
    
    

    相关文章

      网友评论

        本文标题:JavaScript之对象

        本文链接:https://www.haomeiwen.com/subject/bvwdhxtx.html