写给Android/Java开发者的JavaScript精解(3)

Luc24K 8年前
   <p>在JavaScript中,对象是最重要的概念,因为除了基本数据类型,其他的一切都是对象。为此,JavaScript提供了多样的创建对象的方法。同时,函数又是极为特殊的一种对象,因此,JavaScript针对函数也做了许多巧妙的使用优化。</p>    <h2><strong>六、在JavaScript中,创建对象的方法有多少?</strong></h2>    <p>在前面两篇文章中,已经涉及了三种创建对象的方法:</p>    <ul>     <li>new Object()</li>     <li>Object.create()</li>     <li>{ }<br> 三种方法都比较简便。在ES6中,针对大括号({ })语法创建对象进一步做了优化。 <h3><strong>1、再探大括号法创建对象</strong></h3> </li>    </ul>    <p>优化一:函数属性的简写</p>    <p>以前,为对象定义函数要这样写:</p>    <pre>  <code class="language-javascript">var  obj = {      add:  function(a,b){             return a + b;      }  }</code></pre>    <p>现在可以这样写了:</p>    <pre>  <code class="language-javascript">var  obj = {      add(a,b){             return a + b;      }  }</code></pre>    <p>第二种写法乍看是第一种写法的语法糖,然而二者却不完全等价,第二种写法中,函数内部也可以通过add来调用自身,因为第二种写法本质上是这样的:</p>    <pre>  <code class="language-javascript">var  obj = {      add: function add(a,b){             return a + b;      }  }</code></pre>    <p>function 后面的add仅限于在函数体内使用,这一点在该系列第一篇中已经讲明。</p>    <p>优化二:基本数据属性和对象属性的优化</p>    <p>假如以前你这样定义了一个对象:</p>    <pre>  <code class="language-javascript">var a = "foo",   b = 42,   c = {};  var o = {   a: a,   b: b,   c: c  };</code></pre>    <p>那么现在,你可以这样写了:</p>    <pre>  <code class="language-javascript">var a = "foo",   b = 42,   c = {};  var o = { a, b, c };</code></pre>    <p>这种写法就完全是一种语法糖了,简单讲,对于以 prop : prop 方式定义的属性,也就是属性名与代表属性值的变量名称相同,就可以直接写 prop。</p>    <p>优化三:属性名也可以用表达式</p>    <pre>  <code class="language-javascript">var i = 0;  var a = {   ["foo" + ++i]: i,    ["foo" + ++i]: i,   ["foo" + ++i]: i  };</code></pre>    <p>上述代码在创建对象时,JavaScript会首先把中括号([])中的内容当作表达式进行求值,求值的结果再转为字符串作为属性的名称。因此,对于对象a,我们有:</p>    <p>a.foo1 == 1; // true</p>    <p>a.foo2 == 2 ; // true</p>    <p>a.foo3 == 3 ; // true</p>    <h3><strong>2、用函数创建对象</strong></h3>    <p>本系列第一篇中,我们指出,函数是一种特殊的对象,最大的特殊之处在于它是可以被调用的(a object which can be called !),其基本的调用方法与Java是一致的。</p>    <p>实际上, <strong>任何一个函数</strong> 还有一种特殊的调用方式,姑且称之为 <strong>new 调用</strong> ,这种调用会创建一个对象返回。</p>    <p>举例如下:</p>    <pre>  <code class="language-javascript">function  add(a,b){       var result  = a + b ;       return result ;  }</code></pre>    <p>这是一个再普通不过的一个函数,我们可以这样使用它:</p>    <p>var sum = add(5,9) ; //sum的值为14</p>    <p>然而,我们还可以这样使用它:</p>    <p>var obj = new add(5,9) ; // typeof obj == object</p>    <p>上面,我们仅仅是在正常的调用前面加了一个关键字 new ,整个函数的执行逻辑就完全发生了变化,最大的变化是它不再返回函数体内的return语句中的值,而是返回了一个对象!</p>    <p>我想,每一个从Java转来学JavaScript的人,看到这样的情况,都会觉得不可思议吧。</p>    <p>有没有感到在JavaScript的世界中,函数作为一个特殊的对象,似乎凌驾于普通对象之上了?这货竟然可以生成对象!</p>    <p>确实是这样的,如果你学习过JavaScript,你应该会听过一句话,在JavaScript中,函数是一等公民,说的就是函数的这种特殊性。</p>    <p>让我们沉下心来,看看上面的new调用到底是怎么执行的:</p>    <ol>     <li>JavaScript引擎执行到 new 调用所在的行时,它立马明白了,这里不是一个普通的函数调用,而是一个new调用,用户想要通过函数调用生成一个对象,于是JavaScript创建出来一个新的对象,姑且称其为 <strong>obj</strong> 。</li>     <li>然后,JavaScript引擎会将函数的prototype属性所指向的对象设为 <strong>obj</strong> 的原型。<br> 实际上,每个函数都有一个prototype属性。当你用一个函数创建一个对象时,新建对象的原型会被自动设置为函数prototype属性指向的对象。</li>     <li>然后,JavaScript引擎会把关键字 <strong>this</strong> 绑定到新创建的对象 <strong>obj</strong> 上,也就是说,之后在函数体内对this关键字的操作就是对新创建的对象 <strong>obj</strong> 的操作。</li>     <li>之后,JavaScript引擎会根据用户在new调用中传入的参数(本例中为5和9,不同的函数要求的参数也不相同,也可以没有参数)来一句一句执行函数, 如果函数最后没有return语句,那么当函数体执行完毕后,JavaScript会直接把对象 <strong>obj</strong> 返回给调用者。如果函数最后有return语句,JavaScript会判断一下return语句中的返回值是不是一个对象,如果是一个对象,那么就把这个对象返回给调用者,如果return语句返回的是一个基本数据类型,而不是一个对象,那么JavaScript仍然把对象 <strong>obj</strong> 返回给调用者。在本例中,JavaScript会首先执行语句 var result = a + b ; 然后遇到了return语句,JavaScript发现这个return语句返回的是一个基本数据类型,不是一个对象,于是它把对象 <strong>obj</strong> 返回给了我们。<br> 注:在本例中,整个函数体内我们没有对关键字 <strong>this</strong> 进行任何操作,所以对象 <strong>obj</strong> 一直没有发生什么变化。</li>    </ol>    <p>观察上面创建对象的过程,我们发现有几点比较别扭:</p>    <ul>     <li>add函数首字母是小写,new add(5,9)形式上不够优美,在面向对象语言实践中,我们更习惯首字母大写的new调用。</li>     <li>语句 var result = a + b ; 对最后的对象没有什么影响,浪费CPU资源</li>     <li>return语句最后返回一个数字,也没有什么用,还让JavaScript多了一次判断</li>    </ul>    <p>于是,大家就 <strong>约定(仅仅是一个约定,不是语言本身的要求):</strong></p>    <ul>     <li>当你创建一个专门用来生成对象的函数时,就把函数名字的首字母大写</li>     <li>函数体内只保留对最后生成对象有影响的语句,也就是对this有影响的语句</li>     <li>不要最后的return语句,确保this所代表的对象能够返回给用户</li>    </ul>    <p>下面就是一个符合上面约定的例子:</p>    <pre>  <code class="language-javascript">function Person(name, age, sex) {    this.name = name;    this.age = age;    this.sex = sex;  }</code></pre>    <p>你可以这样来使用Person函数:</p>    <pre>  <code class="language-javascript">var milter = new Person("milter", 31, "男");</code></pre>    <p>进一步思考一个问题:上面代码中,我们创建了一个Person对象milter,但此时milter只具有三个属性,并不具有方法,也就是行为能力,那么如何为Person对象添加方法呢?</p>    <p>上面我们提到,当JavaScript看到我们用 new 调用一个函数时,它会首先创建一个对象,并将函数的prototype属性指向的对象作为新建对象的原型。因此,我们可以在Person.prototype中添加方法,这样,所有用函数Person创建的对象都会具有我们添加的方法。</p>    <p>例如,我们可以为Person添加一个run方法:</p>    <pre>  <code class="language-javascript">Person.prototype.run =  function (){  // running  }</code></pre>    <p>那么,所有用Person函数创建的对象现在都具有了run方法,如:</p>    <p>milter.run() ;</p>    <p>神奇的是,当我们改变Person.prototype时,那些在改变之前用Person创建的对象也会随之改变。因为所有用Person创建的对象的原型是同一个Person.prototype对象。这是体现JavaScript动态性的典型例子。</p>    <p>现在我们知道,用Person创建的对象的原型是Person.prototype,那么,问题来了,Person的原型又是什么呢?</p>    <p>答案是:所有的函数的原型都是一个特殊的对象: <strong>Function.prototype</strong> ,该对象中包含了作为一个函数的普遍属性和方法,而Function.prototype的原型又是Object.prototype。所以,Person的原型链是:</p>    <p>Person --->Function.prototype---->Object.prototype</p>    <p>而用Person创建的对象的原型链是:</p>    <p>milter--->Person.prototype---->Object.prototype</p>    <p>也就是说,函数的原型和函数创建的对象的原型不是一回事,一定要搞清楚,初学者很容易将二者混为一谈。</p>    <p>明白了这一点,我们也就知道:</p>    <p>Person.eat = function (){</p>    <p>//eating</p>    <p>}</p>    <p>为Person添加的方法eat并不会被用Person创建的对象所继承,它属于Person函数本身的方法。</p>    <p>只要使用JavaScript的人都遵守上面的约定,那么,每当你看到一个名字首字母大写的函数,你就应当立即反应过来,这个函数是用来创建对象的,你应当用 <strong>new调用</strong> 来使用它,而不是将它当作普通函数使用。</p>    <h3><strong>3、用class创建对象</strong></h3>    <p>看到class,你可能会以为JavaScript中也引入了类的概念,然而实际情况可能会让你失望,用class创建对象,其本质还是用函数创建对象,只不过是包装成了类的形式而已。一个class其实就是一个函数。</p>    <p>我们知道,创建函数有两种方式:声明的方式和表达式的方式,这一点在本系列第1篇中有具体的分析。和创建函数一样,也有两种方法创建一个class,也是声明的方式和表达式的方式,如下所示:</p>    <pre>  <code class="language-javascript">//声明的方式创建class  class Person {    constructor(name, age,sex) {      this.name = name;      this.age = age;      this.sex = sex;    }  };</code></pre>    <p>上述代码以 <strong>声明的方式</strong> 定义了一个class Person。此时,创建一个Person对象的方法是:</p>    <p>var person = new Person('milter',31,'男');</p>    <p>我们发现,这个class Person和上一小节中的函数Person非常像,二者创建对象的方法甚至完全一样!最明显的不一样的地方是class Person中多了一个方法 constructor,这个方法在每个class中 <strong>有且只有唯一的一个</strong> ,其作用是初始化用该类创建的对象。</p>    <p>从本质上看, class Person就是函数Person的一个 <strong>语法糖</strong> ,它使得定义一个创建对象的函数更加容易,语义更加清晰,对我们这些从Java转过来的程序员更加友好。除此之外,这里没有任何神秘的东西。</p>    <p>如果你测试一下 class Person 的类型,像这样</p>    <p>typeof Person</p>    <p>你会得到结果 <strong>function</strong> 。说明class Person 本质上就是一个函数。</p>    <p>但是class Person与函数Person有一点小差别。以声明形式定义的函数Person可以在函数定义之前使用,但是,以声明形式定义的class Person,却必须在定义之后才能使用,这点需要在使用中注意。</p>    <p>我们知道,对于函数Person,可以用表达式的方式定义,如下所示:</p>    <pre>  <code class="language-javascript">var  Person = function (name, age, sex) {    this.name = name;    this.age = age;    this.sex = sex;  }</code></pre>    <p>同理,class Person也可以用表达式的方式定义如下:</p>    <pre>  <code class="language-javascript">//表达式的方式创建class  var  Person = class  {    constructor(name, age,sex) {      this.name = name;      this.age = age;      this.sex = sex;    }  };</code></pre>    <p>在用表达式方式定义函数时,你可以在function 后面添加一个名字,以便在函数体内使用该函数,同样,你也可以在class 后面添加一个名字,以便在class内部使用该class。</p>    <p>在用函数语法创建对象中(参见上一小节),为对象添加 <strong>共同的</strong> 方法和属性需要在Person.prototype中添加,为函数Person本身添加方法和属性需要直接在Person中添加。利用class语法创建对象对此做了很大的优化,请看如下代码:</p>    <pre>  <code class="language-javascript">//声明的方式创建class  class Person {    constructor(name, age,sex) {      this.name = name;      this.age = age;      this.sex = sex;    }      run(){    //running    }    static  sextype(){       return   ['男','女'] ;    }  };</code></pre>    <p>上述代码中,我们进一步为Person创建了两个方法,一个是普通方法run,一个是static方法sextype(请注意:各个方法之间既无逗号分隔,也无分号分隔)。二者有什么区别呢?</p>    <p>普通方法将会被设置成Person.prototype的方法,static方法将会被设置成Person本身的方法。如下图所示:</p>    <table>     <thead>      <tr>       <th>Person</th>       <th>Person.prototype</th>      </tr>     </thead>     <tbody>      <tr>       <td>sextype</td>       <td>run</td>      </tr>     </tbody>    </table>    <p>由此,我们知道,sextype是不会被class Person创建的对象继承的,而只能通过Person.sextype的方法调用。run会被class Person创建的对象继承。</p>    <p>可以看到,当我们将class Person还原成它的本质 <strong>函数</strong> 后,我们就能明白class中的static方法和普通方法的区别,也很容易理解为什么在对象中没法调用static方法。</p>    <p>好奇的你可能会问,那constructor方法被放到哪里了呢?答案是:Person.prototype,它会被所有的对象继承。</p>    <p>但是由于这个方法的特殊性,JavaScript不允许我们直接调用它。也就是说,你不能这样调用:</p>    <p>milter.constructor('lucy',18,'女'); //语法错误</p>    <p>class 语法还允许我们使用extends和super关键字,来看一个例子:</p>    <pre>  <code class="language-javascript">class Point {      constructor(x, y) {          this.x = x;          this.y = y;      }      toString() {          return this.x+':'+this.y;      }  }    class ColorPoint extends Point {      constructor(x, y, color) {          super(x, y);           this.color = color;      }      toString() {          return super.toString() + ' in ' + this.color;       }  }</code></pre>    <p>友情提示:Point 和ColorPoint本质上都是函数。</p>    <p>extends关键字在这里主要有三个作用:</p>    <ul>     <li> <p>将ColorPoint的原型设为 Point,所以此时ColorPoint的原型链成了:</p> <p>ColorPoint --->Point---->Function.prototype---->Object.prototype</p> <p>由此我们可以知道,ColorPoint将会继承Point中的静态方法。</p> </li>     <li>将ColorPoint.prototype的原型设为Point.prototype,所以此时ColorPoint.prototype的原型链成了:<br> ColorPoint.prototype---->Point.prototype---->Object.prototype<br> 由此我们知道,用ColorPoint创建的对象将会继承Point中的普通方法。</li>     <li>强制ColorPoint在其constructor方法中调用Point的constructor方法,调用语法为:<br> super();</li>    </ul>    <p>另一个使用super关键字的地方是:在ColorPoint的toString方法中,通过super.toString()来调用Point中的toString(),这一点和我们在Java中的用法一样,不再赘述。</p>    <p>小结:本文中,我们学习了用大括号法创建对象的许多简便写法。更重要的是学习了用函数创建对象和用class创建对象的方法,并从内在原理上分析了二者的统一性,从本质上认识到了class语法只是对JavaScript的对象原型继承的一层包装而已。在JavaScript中是没有什么类的概念的。</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/6e71ea7d769b</p>    <p> </p>