在16年的10月份,在校内双选会找前端实习的时候,hr问了一个问题:JavaScript的面向对象理解吗?我张口就说“JavaScript是基于原型的!”。而后就没什么好说的了,hr可能不知道原型,我也解释不了,由于我也就知道这一点而已,至于JavaScript到底面不面向对象,如何基于原型的,我都不太清楚。最近又开始找工做了,在掘金看到面试题就赶快看一下,但是一些代码却使我更加的困惑了,决定深刻认真地学习一下JavaScipt面向对象的知识,花了几天的时间看了MDN上的Javacript对象相关的内容仍存疑惑,因而求助于那本有名的书:《You-Dont-Know-JS》的一章 “this & Object Prototypes”连接在最下面(Github上的英文版),个人疑惑也获得了解答,这个过程也是有点痛并快乐着的,写下这篇博客与你们分享一下本身的收获。前端
为了可以清楚的解释这一切,我先从对象讲起。从其余面向对象语言(如Java)而来的人可能认为在JS里的对象也是由类来实例化出来的,而且是由属性和方法组成的。git
实际上在JS里并非如你所想(我开始是这么想的)那样,对象或直接称为object,实际上只是一些映射对的集合,像Map,字典等概念。JS里有大概7种类型(加上Symbol),数字、字符串、null、undefined、布尔、Symbol、对象。除对象之外的其余类型属于原始类型,就是说它们比较单纯,包含的东西比较少,基本上就是字面量所表示的那些(像C语言中的一些类型,就是占那么多空间,没有其余的东西)。object基本上是一些键值对的集合,属于引用类型,便是有一个名字去指向它来供别人使用的,就好像比较重的东西你拿不动,而只是拿了张记录东西所在地的纸条。因此当A对象里嵌套了B对象,仅表示A里面有一个引用指向了B,并非真正把B包含在A里面,虽然看起来是这样(尤为是从对象的字面量上来看),因此才会有所谓的深拷贝与浅拷贝。github
有句话叫“JavaScript里一切皆对象”,是由于在不少状况下原始类型会被自动的转为对象,而函数实际上也是对象,这样这句话看起来就颇有道理了。面试
说明对象的本质是为了正确地认识对象,由于这关系到后面的理解。chrome
JS的世界里有一些对象叫原型,若是你有所怀疑,你能够在chrome终端下打出如下代码来验证它的存在:编程
console.log(Object.prototype); //你能够理解prototype是指向原型的引用数组
和 console.log(typeof Object.prototype);//object编程语言
在看看:函数
console.log(typeof {}.prototype);//undefined学习
为何空对象{}没有prototype对象呢,事实上prototype只是函数对象的一个属性,而Array、Object倒是都是函数,而不是对象或者类(class):
console.log(typeof Object);//function
为何JS里没有函数这样一种类型,而typeof输出的倒是function,即JS把函数也当作了一种类型,这揭示了函数做为一种特殊对象的地位的超然性。
function foo(){console.log('inner foo');};
console.log(typeof foo);//function
console.log(typeof []);//object
与数组这种内建对象相比,说明了函数的地位非比寻常,实际上函数在JS中地位是一等的(或者说你们是平等的),函数能够在参数中传递也说明了这一点,这使得JS具有了一些属于函数式语言的特性。
函数与普通对象的地位相等,使得函数中的"this"关键字极具迷惑性,可能不少人都知道了,this指向的是函数在运行时的上下文,既不是函数对象自己,也不是函数声明时所在做用域,具体是如何指向某个对象的就不在本文的讨论范畴了,感兴趣的能够去看《You-Dont-Know-JS》。
查看以下代码的输出结果:
console.log(foo.prototype);
能够看出foo.prototype是一个大概有两个属性的对象:constructor和__proto__。
console.log(foo.prototype.constructor === foo);//true
能够看出一个函数的原型的constructor属性指向的是函数自己,你能够换成内建的一些函数:Object、String、Number,都是这样的。
在观察foo.prototype的__proto__以前,先考察下面看起来很面向对象的几行代码:
var fooObj = new foo();//inner foo
console.log(fooObj);//看获得,fooObj也有一个__proto__的属性,那么__proto__是什么呢,
console.log(fooObj.__proto__ === foo.prototype);//true
你知道了,对象的__proto__会指向其“构造函数”的prototype(先称之为构造函数)。
new 的做用其实是,新建立一个对象,在这个对象上调用new关键字后面的函数(this指向此对象,虽然这里没有用到),并将对象的__proto__指向了函数的原型,返回这个对象!
为了便于理解以上的内容,我画了这张图:
用绿色代表了重点:foo.prototype,同时函数声明能够这样声明:
var bar = new Function("console.log('inner bar');");
猜想console.log(foo.__proto__ === Function.prototype);输出为true;
的确如此,因而再向图片中加入一些东西:
看起来愈来愈复杂了,仍是没有讲到foo.prototype的__proto__指向那里。
若是把prototype对象当作是一个普通对象的话,那么依据上面获得的规律:
console.log(foo.prototype.__proto__ === Object.prototype);//true
是这样的,从新看一个更常见的例子:
1 function Person(name){ 2 this.name = name; 3 var label = 'Person'; 4 } 5
6 Person.prototype.nickName = 'PersonPrototype'; 7
8 var p1 = new Person('p1'); 9
10 console.log(p1.name);//p1
11 console.log(p1.label);//undefined
12 console.log(p1.nickName);//PersonPrototype
先从图上来看一下上面这些对象的关系:
为何p1.nickName会输出PersonPrototype,这是JS的内在的原型链机制,当访问一个对象的属性或方法时,JS会沿着__proto__指向的这条链路从下往上寻找,找不到就是undefined,这些原型链即图中彩色的线条。
把JS中面向对象的语法的内容放到靠后的位置,是为了避免给读者形成更大的疑惑,由于只有明白了原型及原型链,这些语法的把戏你才能一目了然。
面向对象有三大特性:封装、继承、多态
封装即隐藏对象的一些私有的属性和方法,JS中经过设置对象的getter,setter方法来拦截你不想被访问到的属性或方法,具体有关对象的内部的东西限于篇幅就再也不赘述。
继承是一个面向对象的语言看起来颇有吸引力的特性,以前看一些文章所谓的JS实现继承的多种方式,只会令人更加陷入JS面向对象所形成的迷惑之中。
从原型链的机制出发来谈继承,加入Student要继承Person,那么应当使Sudent.prototype.__proto__指向Person.prototype。
因此借助于__proto__实现继承以下:
1 function Person(name){ 2 this.name = name; 3 var label = 'Person'; 4 } 5
6 Person.prototype.nickName = 'PersonPrototype'; 7
8 Person.prototype.greet = function(){ 9 console.log('Hi! I am ' + this.name); 10 } 11
12 function Student(name,school){ 13 this.name = name; 14 this.school = school; 15 var label = 'Student'; 16 } 17
18 Student.prototype.__proto__ = Person.prototype;19
20 var p1 = new Person('p1'); 21 var s1 = new Student('s1','USTB'); 22 p1.greet();//Hi! I am p1
23 s1.greet();//Hi! I am s1
这时的原型链如图所示:
多态意味着同名方法的实现依据类型有所改变,在JS中只须要在“子类”Student的prototype定义同名方法便可,由于原型链是单向的,不会影响上层的原型。
1 Student.prototype.greet = function() 2 { 3 console.log('Hi! I am ' + this.name + ',my school is ' + this.school); 4 }; 5 s1.greet();//Hi! I am s1,my school is USTB
为何Student和Person的prototype会有constructor指向函数自己呢,这是为了当你访问p1.constructor时会指向Person函数,即构造器(不过没什么实际意义),还有一个极具迷惑性的运算符:instanceof,
instanceof从字面意上来讲就是判断当前对象是不是后面的实例, 实际上其做用是判断一个函数的原型是否在对象的原型链上:
s1 instanceof Student;//true
s1 instanceof Person;//true
s1 instanceof Object;//true
ES6新增的语法使用了 class 和extends来使得你的代码更加的“面向对象”:
1 class Person{ 2 constructor(name){ 3 this.name = name; 4 } 5
6 greet(){ 7 console.log('Hello, I am ' + this.name); 8 } 9 } 10
11 class Student extends Person{ 12 constructor(name, school){ 13 super(name); 14 this.school = school; 15 } 16
17 greet(){ 18 console.log('Hello, I am '+ this.name + ',my school is ' + this.school); 19 } 20 } 21
22 let p1 = new Person('p1'); 23 let s1 = new Student('s1', 'USTB'); 24 p1.greet();//Hello, I am p1
25 p1.constructor === Person;//true
26 s1 instanceof Student;//true
27 s1 instanceof Person;//true
28 s1.greet();//Hello, I am s1my school is USTB
super这个关键字用来引用“父类”的constructor函数,我是很怀疑这多是上面所说的__proto__继承方式的语法糖,不过没有看过源码,并不清楚哈。
你确定已经清楚地明白了JavaScript是如何“面向对象”的了,讽刺地讲,JavaScript不只名字上带了Java,如今就连语法也要看起来像Java了,不过这种掩盖自身语言实现的真实特性,来假装成面向对象的语法只会使得JavaScript更使人迷惑和难以排查错误。
事实上,总有些事情被许多人搞得复杂,繁琐。在《You-Dont-Know-JS》一书中,提供了另外一种组织代码的方式,抛去传统面向对象风格语法带来的复杂的函数原型链,代之以简单对象组成的原型链,称其为行为委托(Behavior Delegation)。
1 var Person = { 2 init: function(name){ 3 this.name = name; 4 }, 5 greet: function(){ 6 console.log('I am ' + this.name); 7 } 8 } 9
10
11 var Student = Object.create(Person); 12
13 Student.init = function(name, school){ 14 Person.init.call(this, name); 15 this.school = school; 16 } 17
18 Student.greet = function(){ 19 console.log('I am '+ this.name + ',my school is ' + this.school); 20 } 21
22 var p1 = Object.create(Person); 23 var s1 = Object.create(Student); 24 p1.init('p1'); 25 p1.greet();//I am p1
26 s1.init('s1','USTB'); 27 s1.greet();//I am s1,my school is USTB
Object.create的做用是以某一对象为原型来建立新的对象,能够简单理解为向下扩展原型链的功能,即生成了一个__proto__指向源对象的新对象。
原型链如图所示:
只是使用了一些对象,实现了和以前代码的一样的功能,而且具备更加简单清晰的原型链,每一个对象之间的关系一目了然,没有了烦人的prototype,简单的原型链能使你更容易分析本身的代码,找出错误所在。
两种组织代码的方式孰优孰劣,大致上是看得出来的,只是面向对象的语法可能看起来令人更熟悉,但我相信不明白具体内在的人必定会迷惑的。
没有其余一门语言像JavaScript同样会在语法层面上给人带来极大的困惑,我想大概是由于JS不只是原型与函数式的混合(已经够糟糕了),其还想方设法地假装成基于类的“面向对象”的语言,并且一些关键词的含义与行为不符。
写这篇文章大概耗费了我5天的时间和很多心血,但这个探索JS内在机制的过程是使人兴奋的,虽不至于深刻到JS的本质,这是一种新奇的体验,同时也使我明白了之后如何去了解一门新接触的语言,透过语言的语法,看出使用某一门语言时的抽象化工做该如何去作,这其实体现了编程语言制造者的思惟。
参考文献:
《You-Dont-Know-JS》 this & Object Prototypes一章 https://github.com/getify/You-Dont-Know-JS/tree/master/this%20%26%20object%20prototypes
MDN JavaScript对象入门 https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects