JavaScript 原型链

MarVroland 3年前
   <p>大部分面向对象的编程语言,都是以“类”( class )作为对象体系的语法基础。 JavaScript 语言中是没有 class 的概念的( <strong>ES6之前</strong> ,ES6中虽然提供了 class 的写法,但实现原理并不是传统的“类” class 概念,仅仅是一种写法), 但是它依旧可以实现面向对象的编程,这就是通过 JavaScript 中的“ <strong>原型对象</strong> ”( prototype )来实现的。</p>    <h2>prototype 属性</h2>    <p>请看这样一个例子:</p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">function Person(name, gender) {      this.name = name;      this.gender = gender;      this.sayHello = function() {          console.log('Hello,I am', this.name, '. I\'m a', this.gender);      };  }</code></pre> </td>      </tr>     </tbody>    </table>    <p>这样定义了一个构造函数,我们创建对象就可以使用这个构造函数作为模板来生成。不过以面向对象的思想来看,不难发现其中的一点问题: name 和 gender 属性是每个实例都各不相同,作为一个自身的属性没有问题,而 sayHello 方法,每个实例对象应该都有,而且都一样,给每个实例对象一个全新的、完全不同(虽然代码内容一样,但 JavaScript 中每个 sayHello 的值都在内存中单独存在)的 sayHello 方法是没有必要的。</p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">var zs = new Person('zhang san', 'male'),      xh = new Person('xiao hong', 'female');    zs.sayHello(); // Hello,I am zhang san . I'm a male  xh.sayHello(); // Hello,I am xiao hong . I'm a female    zs.sayHello === xh.sayHello;  // false</code></pre> </td>      </tr>     </tbody>    </table>    <p>上面代码中展示了 zs.sayHell 和 xh.sayHello 这两个作用相同,而且看起来代码内容也是完全一样的对象,实际是两个独立的,互不相关的对象。</p>    <p>面向对象思想中,是将公共的、抽象的属性和方法提取出来,作为一个基类,子类继承这个基类,从而继承到这些属性和方法。而 JavaScript 中则可以通过 prototype 属性来实现类似的作用。以下是上面代码的改进示例:</p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">function Person(name, gender) {      this.name = name;      this.gender = gender;  }  Person.prototype.sayHello = function() {      console.log('Hello,I am', this.name, '. I\'m a', this.gender);  };    var zs = new Person('zhang san', 'male'),      xh = new Person('xiao hong', 'female');    zs.sayHello(); // Hello,I am zhang san . I'm a male  xh.sayHello(); // Hello,I am xiao hong . I'm a female    zs.sayHello === xh.sayHello;  // true</code></pre> </td>      </tr>     </tbody>    </table>    <p>这时将 sayHello 方法定义到 Person 对象上的 prototype 属性上,取代了在构造函数中给每个实例对象添加 sayHello 方法。可以看到,其还能实现和之前相同的作用,而且 zs.sayHell 和 xh.sayHello 是相同的内容,这样就很贴近面向对象的思想了。那么 zs 和 xh 这两个对象,是怎么访问到这个 sayHello 方法的呢?</p>    <p>在浏览器控制台中打印出 zs ,将其展开,可以看到下面的结果:</p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">zs;  /**   *   Person      gender: "male"      name: "zhang san"      __proto__: Object          constructor: function Person(name, gender)               arguments: null              caller: null              length: 2               name: "Person"              prototype: Object          sayHello:function()              arguments:null              caller:null              length:0              name:""              prototype:Object  */</code></pre> </td>      </tr>     </tbody>    </table>    <p>zs 这个对象只有两个自身的属性 gender 和 name ,这和其构造函数 Person 的模板相同,并且可以在 Person 对象的 __proto__ 属性下找到 sayHello 方法。那么这个 __proto__ 是什么呢?它是 <strong>浏览器环境下</strong> 部署的一个对象,它指的是当前对象的原型对象,也就是构造函数的 prototype 属性。</p>    <p>现在就可以明白了,我们给构造函数 Person 对象的 prototype 属性添加了 sayHello 方法, zs 和 xh 这两个通过 Person 构造函数产生的对象,是可访问到 Person 对象的 prototype 属性的,所以我们定义在 prototype 下的 sayHello 方法, Person 的实例对象都可以访问到。</p>    <p>关于构造函数的 new 命令原理是这样的:</p>    <ol>     <li>创建一个空对象,作为将要返回的对象实例</li>     <li>将这个空对象的原型,指向构造函数的 prototype 属性</li>     <li>将这个空对象赋值给函数内部的 this 关键字</li>     <li>开始执行构造函数内部的代码</li>    </ol>    <h2>constructor 属性</h2>    <p>prototype 下有一个属性 constructor ,默认指向此 prototype 对象所在的构造函数。</p>    <p>如上例中的 zs 下 __proto__ 的 constructor 值为 function Person(name, gender) 。</p>    <p>由于此属性定义在 prototype 属性上,所以它可以在所有的实例对象中获取到。</p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">zs.constructor;  // function Person(name, gender) {  //     this.name = name;  //     this.gender = gender;  // }    zs.hasOwnProperty('constructor'); // false  zs.constructor === Person; // true    zs.constructor === Function; // false  zs.constructor === Object; // false</code></pre> </td>      </tr>     </tbody>    </table>    <p>将 constructor 属性放在 prototype 属性中的一个作用是,可以通过这个属性来判断这个对象是由哪个构造函数产生的,上面代码中, zs 是由 Person 构造函数产生的,而不是 Function 或者 Object 构造函数产生。</p>    <p>constructor 属性的另一个作用就是:提供了一种继承的实现模式。</p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">function Super() {      // ...  }    function Sub() {      Sub.superclass.constructor.call(this);      // ...  }    Sub.superclass = new Super();</code></pre> </td>      </tr>     </tbody>    </table>    <p>上面代码中, Super 和 Sub 都是构造函数,在 Sub 内部的 this 上调用 Super ,就会形成 Sub 继承 Super 的效果, <strong>miniui</strong> 中是这样实现继承的:</p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">mini.Control = function(el) {          mini.Control.superclass.constructor.apply(this, arguments);      // ...  }  // 其中的superclass指代父类的prototype属性</code></pre> </td>      </tr>     </tbody>    </table>    <p>我们自己写一个例子:</p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">// 父类  function Animal(name) {      this.name = name;      this.introduce = function() {          console.log('Hello , My name is', this.name);      }  }  Animal.prototype.sayHello = function() {      console.log('Hello, I am:', this.name);  }    // 子类  function Person(name, gender) {      Person.superclass.constructor.apply(this, arguments);      this.gender = gender;  }  Person.superclass = new Animal();    // 子类  function Dog(name) {      Dog.superclass.constructor.apply(this, arguments);    }  Dog.superclass = new Animal();</code></pre> </td>      </tr>     </tbody>    </table>    <p>基本原理就是在子类中使用父类的构造函数。在 Person 和 Dog 中均没有对 name 属性和 introduce 方法进行操作,只是使用了父类 Animal 的构造函数,就可以将 name 属性和 introduce 方法继承来,请看下面例子:</p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">var zs = new Person('zhang san', 'male');    zs; // Person {name: "zhang san", gender: "male"}  zs.sayHello(); // Uncaught TypeError: zs.sayHello is not a function(…)  zs.introduce(); // Hello , My name is zhang san    var wangCai = new Dog("旺财");    wangCai; // Dog {name: "旺财"}  wangCai.introduce(); // Hello , My name is 旺财</code></pre> </td>      </tr>     </tbody>    </table>    <p>确实实现了我们需要的效果。可是我们发现在调用 zs.sayHello() 时报错了。为什么呢?</p>    <p>其实不难发现问题,我们的 Person.superclass 是 Animal 的一个实例,是有 sayHello 方法的,但是我们在 Perosn 构造函数的内部,只是使用了 Person.superclass.constructor 。而 Person.superclass.constructor 指的仅仅是 Animal 构造函数本身,并没有包括 Animal.prototype ,所以没有 sayHello 方法。</p>    <p>一种改进方法是:将自定义的 superclass 换为 prototype ,即:</p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">function Person(name, gender) {      Person.prototype.constructor.apply(this, arguments);      this.gender = gender;  }  Person.prototype = Animal.prototype;    var zs = new Person('zhang san', 'male');  zs.sayHello(); // Hello, I am: zhang san  zs.introduce() // Hello , My name is zhang san</code></pre> </td>      </tr>     </tbody>    </table>    <p>这样就全部继承到了 Animal.prototype 下的方法。</p>    <p>但是一般不要这样做,上面写法中 Person.prototype = Animal.prototype; 等号两端都是一个完整的对象,进行赋值时, Person.prototype 的原对象完全被 Animal.prototype 替换,切断了和之前原型链的联系,而且此时 Person.prototype 和 Animal.prototype 是相同的引用,给 Person.prototype 添加的属性方法也将添加到 Animal.prototype ,反之亦然,这将引起逻辑混乱。</p>    <p>因此我们在原型上进行扩展是,通常是添加属性,而不是替换为一个新对象。</p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">// 好的写法  Person.prototype.sayHello = function() {      console.log('Hello,I am', this.name, '. I\'m a', this.gender);  };  Person.prototype. // .. 其他属性     // 不好的写法  Person.prototype = {      sayHello:function(){          console.log('Hello,I am', this.name, '. I\'m a', this.gender);      },      // 其他属性方法 ...  }</code></pre> </td>      </tr>     </tbody>    </table>    <h2>JavaScript 原型链</h2>    <p>JavaScript 的所有对象都有构造函数,而所有构造函数都有 prototype 属性(其实是所有函数都有 prototype 属性),所以所有对象都有自己的原型对象。</p>    <p>对象的属性和方法,有可能是定义在自身,也有可能是定义在它的原型对象。由于原型本身也是对象,又有自己的原型,所以形成了一条原型链( <strong>prototype chain</strong> )。</p>    <pre>  <code class="language-javascript">zs.sayHello(); // Hello,I am zhang san . I'm a male    zs.toString(); // "[object Object]"</code></pre>    <p>例如上面的 zs 对象,它的原型对象是 Person 的 prototype 属性,而 Person 的 prototype 本身也是一个对象,它的原型对象是 Object.prototype 。</p>    <p>zs 本身没有 sayHello 方法, JavaScript 通过原型链向上继续寻找,在 Person.prototype 上找到了 sayHello 方法。 toString 方法在 zs 对象本身上没有, Person.prototype 上也没有,因此继续沿原型链查找,最终可以在 Object.prototype 上找到了 toString 方法。</p>    <p>而 Object.prototype 的原型指向 null ,由于 null 没有任何属性,因此原型链到 Object.prototype 终止,所以 Object.prototype 是原型链的最顶端。</p>    <p>“原型链”的作用是,读取对象的某个属性时, <strong>JavaScript</strong> 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的 Object.prototype 还是找不到,则返回 undefined 。</p>    <p>如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”( overiding )。</p>    <p>JavaScript中通过原型链实现了类似面向对象编程语言中的继承,我们在复制一个对象时,只用复制其自身的属性即可,无需将整个原型链进行一次复制, Object.prototype 下的 hasOwnProperty 方法可以判断一个属性是否是该对象自身的属性。</p>    <p>实例对象、 <strong>构造函数</strong> 、 prototype 之间的关系可用下图表示:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/0c8883d6ec7d29e65f53c82f8473e3a9.jpg"></p>    <h2>instranceof 运算符</h2>    <p>instanceof 运算符返回一个布尔值,表示指定对象是否为某个构造函数的实例。由于原型链的关系,所谓的实例并不一定是某个构造函数的直接实例,更准确的描述,应该是: <strong>返回一个后者的原型对象是否在前者的原型链上</strong></p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">zs instanceof Person; // true  zs instanceof Object ;// true     var d = new Date();  d instanceof Date; // true  d instanceof Object; // true</code></pre> </td>      </tr>     </tbody>    </table>    <h2>原型链相关属性和方法</h2>    <h3>Object.prototype.hasOwnProperty()</h3>    <p>hasOwnProperty() 方法用来判断某个对象是否含有指定的自身属性。这个方法可以用来检测一个对象是否含有特定的自身属性,和 in 运算符不同,该方法会忽略掉那些从原型链上继承到的属性。</p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">zs.hasOwnProperty('name'); // true  zs.hasOwnProperty('gender'); // true    zs.hasOwnProperty('sayHello'); // fasle  Person.prototype.hasOwnProperty('sayHello'); // true     zs.hasOwnProperty('toString'); // fasle  Object.prototype.hasOwnProperty('toString'); // true</code></pre> </td>      </tr>     </tbody>    </table>    <h3>Object.prototype.isPrototypeOf()</h3>    <p>对象实例的 isPrototypeOf 方法,用来判断一个对象是否是另一个对象的原型。</p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">var o1 = {};  var o2 = Object.create(o1);  var o3 = Object.create(o2);    o2.isPrototypeOf(o3) // true  o1.isPrototypeOf(o3) // true</code></pre> </td>      </tr>     </tbody>    </table>    <p>上面代码表明,只要某个对象处在原型链上, isProtypeOf 都返回 true 。</p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">Object.prototype.isPrototypeOf({}) // true  Object.prototype.isPrototypeOf([]) // true  Object.prototype.isPrototypeOf(/xyz/) // true  Object.prototype.isPrototypeOf(Object.create(null)) // false</code></pre> </td>      </tr>     </tbody>    </table>    <p>看起来这个方法和 instanceof 运算符作用类似,但 <strong>实际使用是不一样的</strong> 。</p>    <p>例如:</p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">zs instanceof Person ; // true;    Person.isPrototypeOf(zs);// false  Person.prototype.isPrototypeOf(zs); // true</code></pre> </td>      </tr>     </tbody>    </table>    <p>zs instanceof Person 可理解为判断 Person.prototype 在不在 zs 的原型链上。 而 Person.isPrototypeOf(zs) 指的就是 Person 本身在不在 zs 的原型链上,所以返回 false ,只有 Person.prototype.isPrototypeOf(zs) 才为 true 。</p>    <h3>Object.getPrototypeOf()</h3>    <p>ES5 Object.getPrototypeOf 方法返回一个对象的原型。这是获取原型对象的标准方法。</p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">// 空对象的原型是Object.prototype  Object.getPrototypeOf({}) === Object.prototype  // true    // 函数的原型是Function.prototype  function f() {}  Object.getPrototypeOf(f) === Function.prototype  // true    // f 为 F 的实例对象,则 f 的原型是 F.prototype  var f = new F();  Object.getPrototypeOf(f) === F.prototype  // true    Object.getPrototypeOf("foo");  // TypeError: "foo" is not an object (ES5 code)  Object.getPrototypeOf("foo");  // String.prototype                  (ES6 code)</code></pre> </td>      </tr>     </tbody>    </table>    <p>此方法是 <strong>ES5</strong> 方法,需要IE9+。在 <strong>ES5</strong> 中,参数只能是对象,否则将抛出异常,而在 <strong>ES6</strong> 中,此方法可正确识别原始类型。</p>    <h3>Object.setPrototypeOf()</h3>    <p>ES5 Object.setPrototypeOf 方法可以为现有对象设置原型,返回一个新对象。接受两个参数,第一个是现有对象,第二个是原型对象。</p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">var a = {x: 1};  var b = Object.setPrototypeOf({}, a);  // 等同于  // var b = {__proto__: a};    b.x // 1</code></pre> </td>      </tr>     </tbody>    </table>    <p>上面代码中, b 对象是 Object.setPrototypeOf 方法返回的一个新对象。该对象本身为空、原型为 a 对象,所以 b 对象可以拿到 a 对象的所有属性和方法。 b 对象本身并没有 x 属性,但是JavaScript引擎找到它的原型对象 a ,然后读取 a 的 x 属性。</p>    <p>new 命令通过构造函数新建实例对象,实质就是将实例对象的原型,指向构造函数的 prototype 属性,然后在实例对象上执行构造函数。</p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">var F = function () {    this.foo = 'bar';  };    // var f = new F();等同于下面代码  var f = Object.setPrototypeOf({}, F.prototype);  F.call(f);</code></pre> </td>      </tr>     </tbody>    </table>    <h3>Object.create()</h3>    <p>ES5 Object.create 方法用于从原型对象生成新的实例对象,它接收两个参数:第一个为一个对象,新生成的对象完全继承前者的属性(即新生成的对象的原型此对象);第二个参数为一个属性描述对象,此对象的属性将会被添加到新对象。</p>    <p>上面代码举例:</p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">var zs = new Person('zhang san', 'male');    var zs_clone = Object.create(zs);    zs_clone; // {}  zs_clone.sayHello(); // Hello,I am zhang san . I'm a male  zs_clone.__proto__ === zs; // true  // Person  //  __proto__: Person  //      gender: "male"  //      name: "zhang san"  //      __proto__: Object</code></pre> </td>      </tr>     </tbody>    </table>    <p>可以 看出 创建的新对象 zs_clone 的原型为 zs ,从而获得了 zs 的全部属性和方法。但是其自身属性为空,若需要为新对象添加自身属性,则使用第二个参数即可。</p>    <table>     <tbody>      <tr>       <td> </td>       <td> <pre>  <code class="language-javascript">var zs_clone = Object.create(zs, {      name: { value: 'zhangsan\'s clone' },      gender: { value: 'male' },      age: { value: '25' }  });  zs_clone; // Person {name: "zhangsan's clone", gender: "male", age: "25"}</code></pre> </td>      </tr>     </tbody>    </table>    <h2>参考链接</h2>    <ul>     <li> <p><a href="/misc/goto?guid=4959742427905536307" rel="nofollow,noindex">JS中的prototype - 轩脉刃</a></p> </li>     <li> <p><a href="/misc/goto?guid=4959742427993047342" rel="nofollow,noindex">prototype 对象 - JavaScript标准参考教程</a></p> </li>    </ul>    <p> </p>    <p>来自:http://blog.cdswyda.com/post/javascript/2016-11-21-javascript-prototype</p>    <p> </p>