JavaScript 原型链
MarVroland
8年前
<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>