初识 JavaScript 对象的时候,我觉得 JS 是没有继承这种说法的,虽然说 JS 是一门面向对象语言,但是面向对象的一些特性在 JS 中并不存在(好比多态,不过严格来讲也没有继承)。这就困惑了我很长的时间,当我学习到 JS 原型的时候,我才发现了 JS 的新世界。本篇文章讲解了 JavaScript new 操做符与对象的关系、原型和对象关联(也就是俗称的继承)的原理,适合有必定基础的同窗阅读。javascript
许多书籍上都会说到如何在 JS 当中定义“类”,一般来说就是使用以下代码:html
1 function foo () { 2 this.x = 1; 3 this.y = 2; 4 } 5 var obj = new foo(); //{x:1, y:2}
实际上这一个很糟糕的语言机制,咱们首先要明确,在 JS 当中根本没有“类”这种东西。在了解它以前,咱们要先来了解下 JS 的发展历史。java
JavaScript 随着互联网和浏览器而诞生,在早些年代,互联网还比较贫乏,上网的成本也比较高,网速很是的慢,一般须要花很长的时间才能传输完一个纯文本的 HTML 文件。因此那时候 Netscape 就提出,须要有一种解决方案,能使一些操做在客户端进行而不须要经过服务器处理,好比用户在填写邮箱的时候少写了一个“@”,在客户端就能够检查出错误并提示用户而不须要在服务器进行解析,这样就能够极大的下降通讯操做带来了延迟和带宽消耗。而那时候,正巧 JAVA 问世,火的那叫个一塌糊涂,因此 Netscape 决定和 SUN 合做,在浏览器当中植入 JAVA 小程序(后来称Java applet)。不事后来就这一方案产生了争议,由于浏览器原本只须要很小的操做,而 JAVA 语言自己太“重”了,用来处理什么表单验证的问题实在是大材小用,因此决定开发一门新的语言来支持客户端的轻量级操做,而又要借鉴 JAVA 的语法。因而乎 Netscape 开发出了一门新的轻量级语言,在语法方面偏向于 C 和 JAVA,在数据结构方面偏向于 JAVA,这门语言最初叫作 Mocha,后来通过多年的演变,变成了如今的 JavaScript。程序员
故事说道这里,好像和本文并无什么关系...别急,立刻就要说道点子上了。这个语言为何要取名 JavaScript 呢,其实它和 JAVA 并无半毛钱的关系,只是由于在那点年代,面向对象方法问世才不久,全部的程序员都推崇学习面向对象方法,再加上 JAVA 的横空出世和大力宣传,只要和 JAVA 沾边的东西就像是往脸上贴了金同样,自带光环。因此便借助了 JAVA 的名气来进行宣传,不过光是嘴皮子宣传还不行,由于面向对象方法的推崇,你们都习惯于面向对象的语法,也就是 new Class() 的方法编写代码。不过 JavaScript 语言自己并无类的概念,其是多种语言的大杂烩,为了更加贴合习惯了面向对象语法的程序员,因而 new 操做符诞生了。编程
好了,说了这么大一堆故事,就是想告诉同窗们,new 操做符在 JavaScript 当中自己就是一个充满歧义的东西,它并不存在类的概念,只是贴合程序员习惯而已。那么在 JavaScript 当中 new 操做符和对象究竟有什么关系呢?思考下面这一段代码:小程序
1 function foo () { 2 this.x = 1; 3 this.y = 2; 4 return { 5 z:3 6 } 7 } 8 var obj = new foo(); //{z:3}
咦?发生了什么奇怪的事情,x 和 y 哪里去了?实际上 new 操做符并非传统面向对象语言那样,建立一个类的实例,new 操做符实际上只是在引擎内部帮咱们在函数的开始建立好了一个对象,而后将函数的上下文绑定到这个对象上面,并在函数的末尾返回这个对象。这里须要注意的问题是,若是咱们手动的返回了一个对象,那么按照函数执行机制,一旦返回了一个值,那么该函数也就执行结束,后面的代码将不会执行,因此说在刚才的例子中咱们获得的对象只是咱们手动定义的对象,并非引擎帮咱们建立的对象。 new 操做符实际上相似于如下操做:数组
1 function foo () { 2 //新建立一个对象,将 this 绑定到该对象上 3 4 //在这里编写咱们想要的代码 5 6 //return this; 7 }
不过须要注意的是,new 操做符只接受 Object 类型的值,若是咱们手动返回的是基本类型,则仍是会返回 this :浏览器
1 function foo () { 2 this.x = 1; 3 this.y = 2; 4 return 0; 5 } 6 var obj = new foo(); //{x:1, y:2}
如今咱们如今能够将 new 操做符定义成如下方法:服务器
1 function newOpertor (cls, ...args) { 2 var obj = {}; 3 cls.apply(obj, args); 4 return obj; 5 } 6 7 function foo (x, y) { 8 this.x = x; 9 this.y = y; 10 } 11 12 var obj = newOpertor(foo, 1, 2); //{x:1, y:2}
JavaScript 中存在相似继承的机制,可是又不是标准面向对象的继承,在 JS 中使用的是原型的机制。要记住,在 JS 中只有对象,没有类,对象的继承是由原型来实现,笼统的来讲能够这样理解,一个对象是另外一个对象的原型,那么即可以把它比做父类,子类既然也就继承了父类的属性和方法。数据结构
1 function foo () { 2 this.x = 1; 3 this.y = 2; 4 } 5 6 foo.prototype.z = 3 7 8 var obj = new foo(); 9 console.log(obj.z); //3
[[prototype]] 是函数的一个属性,这个属性的值是一个对象,该对象是全部以该函数为构造器创造的对象的原型。能够把它近似的理解为父类对象,那么相应的,子类天然会继承父类的属性和方法。不过为何要区分原型继承和类继承的概念呢?标准的面向对象方法,类是不具备实际内存空间,只是一个事物的抽象,对象才是事物的实体,而经过继承获得的属性和方法,同属于该对象,不一样的对象各自都拥有独立的继承而来的属性。不过在 JavaScript 当中,因为没有类的概念,一直都是对象,因此咱们“继承”的,是一个具备实际内存空间的对象,也是实体,也就是说,全部新建立的子对象,他们共享一个父对象(后面我统称为原型),不会拥有独立的属性:
1 function foo () { 2 this.x = 1; 3 this.y = 2; 4 } 5 6 foo.prototype.z = 3 7 8 var obj1 = new foo(); 9 10 console.log(obj1.z); //3 11 12 foo.prototype.z = 2 13 14 console.log(obj1.z); //2
还记得咱们以前所说的 new 操做符的原理吗?new 操做符的本质不是实例化一个类,而是引擎贴合习惯了面向对象编程方法的程序员,因此说 [[prototype]] 属性本质上也是 new 操做符的一个副产物。这个属性只在函数上面有意义,该属性定义了 new 操做符产生的对象的原型。除了 [[prototype]] 能够访问到对象原型之外,还有一个非标准的方法,在每个对象中都有一个 __proto__ 属性,这个属性直接关联到了该对象的原型。这种方法没有写入 W3C 的标准规范,可是却获得了浏览器的普遍支持,许多浏览器都提供了该方法以供访问对象的原型。(我的以为 __proto__ 比 [[prototype]] 更能体现原型链的本质)
1 function foo () { 2 this.x = 1; 3 this.y = 2; 4 } 5 6 foo.prototype.z = 3 7 8 var obj1 = new foo(); 9 10 console.log(obj1.__proto__); //{z:3}
除了使用 new 操做符和函数的 [[prototype]] 属性定义对象的原型以外,咱们还能够直接在对象上显示的经过 __proto__ 来定义,这种定义对象原型的方式更可以体现出 JavaScript 语言的本质,更可以使初学者理解原型链继承的机制。
1 var father = {x:1}; 2 3 var child = { 4 y:2, 5 __proto__:father 6 }; 7 8 console.log(child.x); //1
如今咱们来完成以前那个自定义 new 操做(若是你还不能理解这个函数,没有关系,跳过它,这并不影响你接下来的学习):
1 function newOpertor (cls, ...args) { 2 var obj = Object.create(cls.prototype); 3 cls.apply(obj, args); 4 return obj; 5 } 6 7 function foo (x, y) { 8 this.x = x; 9 this.y = y; 10 } 11 12 foo.prototype.z = 3 13 14 var obj1 = newOpertor(foo, 1, 2) 15 16 console.log(obj1.z); //3
介绍完原型以后,同窗们须要明确如下几个概念:
那么 JavaScript 当中的原型是如何实现相互关联的呢?JS 引擎又是如何查找这些关联的属性呢?如何实现多个对象的关联造成一条原型链呢?
1 var obj1 = { 2 x:1 3 } 4 5 var obj2 = { 6 y:2, 7 __proto__:obj1 8 } 9 10 var obj3 = { 11 z:3, 12 __proto__:obj2 13 } 14 15 console.log(obj3.y); //2 16 console.log(obj3.x); //1
在上面这段代码,咱们能够看出,对象的原型能够实现多层级的关联的操做,obj1 是 obj2 的原型, obj2 同时又是 obj3 的原型,这种多层级的原型关联,就是咱们常说的原型链。在访问一个处于原型链当中的对象的属性,会沿着原型链对象一直向上查找,咱们能够把这种原型遍历操做当作是一个单向的链表,每个处于原型链的对象都是链表当中的一个节点,JS 引擎会沿着这条链表一层一层的向下查找属性,若是找到了一个与之匹配的属性名,则返回该属性的值,若是在原型链的末端(也就是 Object.prototype)都没有找到与之匹配的属性,则返回 undefined。要注意这种查找方式只会返回第一个与之匹配的属性,因此会发生属性屏蔽:
1 var obj1 = { 2 x:1 3 } 4 5 var obj2 = { 6 x:2, 7 __proto__:obj1 8 } 9 10 var obj3 = { 11 x:3, 12 __proto__:obj2 13 } 14 15 console.log(obj3.x); //3
若要访问原型的属性,则须要一层的一层的先向上访问原型对象:
1 console.log(obj3.__proto__.x); //2 2 console.log(obj3.__proto__.__proto__.x); //1
要注意的一点是,原型链的遍历只会发生在 [[getter]] 操做上,也就是取值操做,也能够称之为右查找(RHS)。相反,如果进行 [[setter]] 操做,也就是赋值操做,也能够称做左查找(LHS),则不会遍历原型链,这条原则保证了咱们在对对象进行操做的时候不会影响到原型链:
1 var obj1 = { 2 x:1 3 } 4 5 var obj2 = { 6 __proto__:obj1 7 } 8 9 console.log(obj2.x); //1 10 11 obj2.x = 2; 12 13 console.log(obj2.x); //2 14 console.log(obj1.x); //1(并无发生变化)
在遍历原型链中,若是访问带有 this 引用的方法,可能会发生令你意想不到的结果:
1 var obj1 = { 2 x:1, 3 foo: function () { 4 console.log(this.x); 5 } 6 } 7 8 var obj2 = { 9 x:2, 10 __proto__:obj1 11 } 12 13 obj2.foo(); //2
在上面的内容中,咱们讨论过,对象的原型至关于父类,咱们能够继承它所拥有的属性和方法,因此在咱们访问 foo() 函数的时候时候,实际上调用该方法的对象是 obj2 而不是 obj1。关于更详细的内容,须要了解 this 和上下文绑定,这不在本篇文章的讨论范围以内。
关于原型链的问题,你们须要理解的一点是,任何对象的原型链终点,都是 Object.prototype,能够把 Object 理解为全部对象的父类,相似于 JAVA 同样,因此说全部对象均可以调用一些 Object.prototype 上面的方法,好比 Object.prototype.valueOf() 以及 Object.prototype.toString() 等等。全部的 string 类型,其原型为 String.prototype ,String.prototype 是一个对象,因此其原型也就是 Object.prototype。这就是咱们为何可以在一个 string 类型的值上调用一些方法,好比 String.prototype.concat() 等等。同理全部数组类型的值其原型是 Array.prototype,数字类型的值其原型是 Number.prototype:
1 console.log({}.__proto__ === Object.prototype); //true 2 3 console.log("hello".__proto__ === String.prototype); //true 4 5 console.log(1..__proto__ === Number.prototype); //true 6 //注意用字面量访问数字类型方法时,第一个点默认是小数标志 7 8 console.log([].__proto__ === Array.prototype); //true
理解了原型链的遍历操做,咱们如今就能够学习如何添加属于本身的方法。咱们如今知道了全部字符串的原型都是 String.prototype ,那么咱们能够对其进行修改来设置咱们本身的内置方法:
1 String.prototype.foo = function () { 2 return this + " foo"; 3 } 4 5 console.log("bar".foo()); //bar foo
因此说,在处理一些浏览器兼容性问题的时候,咱们能够直接修改内置对象来兼容一些旧浏览器不支持的方法,好比 String.prototype.trim() :
1 if (!String.prototype.trim) { 2 String.prototype.trim = function() { 3 return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); 4 }; 5 }
不过须要注意,切忌随意修改内置对象的原型方法,一是由于这会带来额外的内存消耗,二是这可能会在系统中形成一些隐患,通常只是用来作浏览器兼容的 polyfill 。
for ... in 语句会遍历原型链上全部可枚举的属性(关于属性的可枚举性质,能够参考 《JavaScript 常量定义》),有时咱们在操做的时候须要忽略掉原型链上的属性,只访问该对象上的属性,这时候咱们可使用 Object.prototype.hasOwnProperty() 方法来判断属性是否属于原型属性:
1 var obj1 = { 2 x:1, 3 } 4 5 var obj2 = { 6 y:2, 7 __proto__:obj1 8 } 9 10 for(var key in obj2){ 11 console.log(obj2[key]); //2, 1 12 } 13 14 for(var key in obj2){ 15 if(obj2.hasOwnProperty(key)){ 16 console.log(obj2[key]); //2 17 } 18 }
咱们知道经过 new 操做符建立的对象能够经过 instanceof 关键字来查看对象的“类”:
1 function foo () {} 2 3 var obj = new foo(); 4 5 console.log(obj instanceof foo); //true
实际上这个操做也是不严谨的,咱们如今已经知道了 new 操做符在 JavaScript 当中本是一个具备歧义设计,instanceof 操做符自己也是一个会让人误解的操做符,它并无实例这种说法,实际上这个操做符只是判断了对象与函数原型的关联性,也就是说其返回的是表达式 object.__proto__ === function.prototype 的值。
1 function foo () {} 2 3 var bar = { 4 x:1 5 } 6 7 foo.prototype = bar 8 9 var obj = { 10 __proto__: bar 11 } 12 13 console.log(obj instanceof foo); //true
在这一段代码中,咱们能够看出 obj 和 foo 并无任何关系,只是 obj 的原型和 foo.prototype 关联到了同一个对象上面,因此其结果会返回 true。
不过对基本类型类型使用 instanceof 方法的话,可能会产生意外的结果:
1 console.log("1" instanceof String); //false 2 3 console.log(1 instanceof Number); //false 4 5 console.log(true instanceof Boolean); //false
可是咱们一样可使用使用字面量调用原型的方法,这可能会让人感到困惑,不过咱们不用担忧它,并非原型链出现什么毛病,而是在对基本类型进行字面量操做的时候,会涉及到隐式转换的问题。JS 引擎会先将字面量转换成内置对象,而后在调用上面的方法,隐式转换问题不在本文的讨论范围之类,你们能够参考 Kyle Simpson — 《你不知道的 JavaScript (中卷)》。
实际对象的 Object.prototype.isPrototypeOf() 方法更能体现出对象原型链的关系,此方法判断一个对象是不是另外一个对象的原型,不一样于 instanceof 的是,此方法会遍历原型链上全部的节点,此方法做用于对象,而 instanceof 方法做用于构造器,其都会遍历原型链上全部的节点:
1 var obj1 = { 2 } 3 4 var obj2 = { 5 __proto__:obj1 6 } 7 8 var obj3 = { 9 __proto__:obj2 10 } 11 12 console.log(obj2.isPrototypeOf(obj3)); //true 13 console.log(obj1.isPrototypeOf(obj3)); //true 14 console.log(Object.prototype.isPrototypeOf(obj3)); //true
在 ES5 当中拥有标准方法 Object.getPrototypeOf() 能够供咱们得到一个对象的原型,在ES6 当中拥有新的方法 Object.setPrototypeOf() 能够设置一个对象的原型,不过在使用以前请先查看浏览器兼容性。
1 var obj1 = { 2 x:1 3 } 4 5 var obj2 = { 6 y:2 7 } 8 9 Object.setPrototypeOf(obj2, obj1); 10 11 console.log(Object.getPrototypeOf(obj2) === obj1); //true
咱们如今知道,经过 new 操做符建立的对象,其原型会关联到函数的 [[prototype]] 上面,实际上这是一个很糟糕的写法,一味的贴合面向对象风格的编程模式,使得不少人没法领域 JavaScript 当中的精髓。许多书籍都会写到 JavaScript 中有许多奇怪的地方,而后教你如何避开这些地雷,实际上这不是一个好的作法,并非由于 JavaScript 是一门稀奇古怪的语言,而是咱们不肯意去面对它的特性,正确的理解这些特性,才能让咱们写出更加高效的程序。Object.create() 方法对于对象之间的关联和原型链的机制更加清晰,比 new 操做符更加可以理解 JavaScript 的继承机制。该方法建立一个新对象,并使新对象的原型关联到参数对象当中:
1 var obj1 = { 2 x:1 3 } 4 5 var obj2 = Object.create(obj1); 6 7 console.log(obj1.isPrototypeOf(obj2)); //true
不过使用的时候还须要注意浏览器的兼容性,下面给出 MDN 上面的 polyfill:
1 (function() { 2 if (typeof Object.create != 'function') { 3 Object.create = (function() { 4 function Temp() {} 5 var hasOwn = Object.prototype.hasOwnProperty; 6 return function(O) { 7 if (typeof O != 'object') { 8 throw TypeError('Object prototype may only be an Object or null'); 9 } 10 Temp.prototype = O; 11 var obj = new Temp(); 12 Temp.prototype = null; 13 if (arguments.length > 1) { 14 var Properties = Object(arguments[1]); 15 for (var prop in Properties) { 16 if (hasOwn.call(Properties, prop)) { 17 obj[prop] = Properties[prop]; 18 } 19 } 20 } 21 return obj; 22 }; 23 })(); 24 } 25 })();
关于 Object.create() 方法要注意的一点是,若是参数为 null 那么会建立一个空连接的对象,因为这个对象没有任何原型链,因此说它不具备任何原生的方法,也没法进行原型的判断操做,这种特殊的对象常被称做“字典”,它彻底不会受原型链的干扰,因此说适合用来存储数据:
1 var obj = Object.create(null); 2 obj.x = 1 3 4 var bar = Object.create(obj); 5 bar.y = 2; 6 7 console.log(Object.getPrototypeOf(obj)); //null 8 9 console.log(Object.prototype.isPrototypeOf(obj)); //false 10 11 console.log(obj instanceof Object); //false 12 13 console.log(bar.x); //1 14 15 obj.isPrototypeOf(bar); //TypeError: obj.isPrototypeOf is not a function 16 17 /** 18 * 注意因为对象没有关联到 Object.prototype 上面,因此没法调用原生方法,但这并不影响此对象的关联操做。 19 */
原型链是 JavaScript 当中很是重要的一点,同时也是比较难理解的一点,由于其与传统的面向对象语言有着很是大的区别,但这是正是 JavaScript 这门语言的精髓所在,关于原型与原型链,咱们须要知道如下这几点:
关于 JavaScript 原型链,在一开始人们都称为“继承”,其实这是一种不严谨的说法,由于这不是标准的面向对象方法,不过初期人人经常这么理解。如今我每每称之为关联和委托,关联指的是一个对象关联到另外一个对象上,而委托则指的是一个对象能够调用另外一个对象的方法。
本篇文章均为我的理解,若有不足或纰漏,欢迎在评论区指出。
Kyle Simpson — 《你不知道的 JavaScript (上卷)》
MDN — Object - JavaScript | MDN
阮一峰 — JavaScript 语言的历史