一、引言

在16年的10月份,在校内双选会找前端实习的时候,hr问了一个问题:JavaScript的面向对象理解吗?我张口就说“JavaScript是基于原型的!”。然后就没什么好说的了,hr可能不知道原型,我也解释不了,因为我也就知道这一点而已,至于JavaScript到底面不面向对象,如何基于原型的,我都不太清楚。最近又开始找工作了,在掘金看到面试题就赶快看一下,可是一些代码却使我更加的困惑了,决定深入认真地学习一下JavaScipt面向对象的知识,花了几天的时间看了MDN上的Javacript对象相关的内容仍存疑惑,于是求助于那本有名的书:《You-Dont-Know-JS》的一章 “this & Object Prototypes”链接在最下面(Github上的英文版),我的疑惑也得到了解答,这个过程也是有点痛并快乐着的,写下这篇博客与大家分享一下自己的收获。

二、JavaScript的对象

为了能够清楚的解释这一切,我先从对象讲起。从其他面向对象语言(如Java)而来的人可能认为在JS里的对象也是由类来实例化出来的,并且是由属性和方法组成的。

实际上在JS里并不是如你所想(我开始是这么想的)那样,对象或直接称为object,实际上只是一些映射对的集合,像Map,字典等概念。JS里有大概7种类型(加上Symbol),数字、字符串、null、undefined、布尔、Symbol、对象。除对象以外的其他类型属于原始类型,就是说它们比较单纯,包含的东西比较少,基本上就是字面量所表示的那些(像C语言中的一些类型,就是占那么多空间,没有其他的东西)。object基本上是一些键值对的集合,属于引用类型,即是有一个名字去指向它来供别人使用的,就好像比较重的东西你拿不动,而只是拿了张记录东西所在地的纸条。所以当A对象里嵌套了B对象,仅表示A里面有一个引用指向了B,并不是真正把B包含在A里面,虽然看起来是这样(尤其是从对象的字面量上来看),所以才会有所谓的深拷贝与浅拷贝。

有句话叫“JavaScript里一切皆对象”,是因为在很多情况下原始类型会被自动的转为对象,而函数实际上也是对象,这样这句话看起来就很有道理了。

说明对象的本质是为了正确地认识对象,因为这关系到后面的理解。

三、原型也是对象

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

是这样的,重新看一个更常见的例子:

 function Person(name){
this.name = name;
var label = 'Person';
} Person.prototype.nickName = 'PersonPrototype'; var p1 = new Person('p1'); console.log(p1.name);//p1
console.log(p1.label);//undefined
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__实现继承如下:

 function Person(name){
this.name = name;
var label = 'Person';
} Person.prototype.nickName = 'PersonPrototype'; Person.prototype.greet = function(){
console.log('Hi! I am ' + this.name);
} function Student(name,school){
this.name = name;
this.school = school;
var label = 'Student';
} Student.prototype.__proto__ = Person.prototype;
var p1 = new Person('p1');
var s1 = new Student('s1','USTB');
p1.greet();//Hi! I am p1
s1.greet();//Hi! I am s1

这时的原型链如图所示:

多态意味着同名方法的实现依据类型有所改变,在JS中只需要在“子类”Student的prototype定义同名方法即可,因为原型链是单向的,不会影响上层的原型。

 Student.prototype.greet = function()
{
console.log('Hi! I am ' + this.name + ',my school is ' + this.school);
};
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来使得你的代码更加的“面向对象”:

 class Person{
constructor(name){
this.name = name;
} greet(){
console.log('Hello, I am ' + this.name);
}
} class Student extends Person{
constructor(name, school){
super(name);
this.school = school;
} greet(){
console.log('Hello, I am '+ this.name + ',my school is ' + this.school);
}
} let p1 = new Person('p1');
let s1 = new Student('s1', 'USTB');
p1.greet();//Hello, I am p1
p1.constructor === Person;//true
s1 instanceof Student;//true
s1 instanceof Person;//true
s1.greet();//Hello, I am s1my school is USTB

super这个关键字用来引用“父类”的constructor函数,我是很怀疑这可能是上面所说的__proto__继承方式的语法糖,不过没有看过源码,并不清楚哈。

你肯定已经清楚地明白了JavaScript是如何“面向对象”的了,讽刺地讲,JavaScript不仅名字上带了Java,现在就连语法也要看起来像Java了,不过这种掩盖自身语言实现的真实特性,来伪装成面向对象的语法只会使得JavaScript更令人迷惑和难以排查错误。

七、另一种方式

事实上,总有些事情被许多人搞得复杂,繁琐。在《You-Dont-Know-JS》一书中,提供了另一种组织代码的方式,抛去传统面向对象风格语法带来的复杂的函数原型链,代之以简单对象组成的原型链,称其为行为委托(Behavior Delegation)。

 var Person = {
init: function(name){
this.name = name;
},
greet: function(){
console.log('I am ' + this.name);
}
} var Student = Object.create(Person); Student.init = function(name, school){
Person.init.call(this, name);
this.school = school;
} Student.greet = function(){
console.log('I am '+ this.name + ',my school is ' + this.school);
} var p1 = Object.create(Person);
var s1 = Object.create(Student);
p1.init('p1');
p1.greet();//I am p1
s1.init('s1','USTB');
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

JavaScript是如何面向对象的的更多相关文章

  1. 前端开发:面向对象与javascript中的面向对象实现(二)构造函数与原型

    前端开发:面向对象与javascript中的面向对象实现(二)构造函数与原型 前言(题外话): 有人说拖延症是一个绝症,哎呀治不好了.先不说这是一个每个人都多多少少会有的,也不管它究竟对生活有多么大的 ...

  2. 简单分析JavaScript中的面向对象

    初学JavaScript的时候有人会认为JavaScript不是一门面向对象的语言,因为JS是没有类的概念的,但是这并不代表JavaScript没有对象的存在,而且JavaScript也提供了其它的方 ...

  3. 前端开发:面向对象与javascript中的面向对象实现(一)

    前端开发:面向对象与javascript中的面向对象实现(一) 前言: 人生在世,这找不到对象是万万不行的.咱们生活中,找不到对象要挨骂,代码里也一样.朋友问我说:“嘿,在干嘛呢......”,我:“ ...

  4. javascript高级特性(面向对象)

    javascript高级特性(面向对象): * 面向对象: * 面向对象和面向过程的区别: * 面向对象:人就是对象,年龄\性别就是属性,出生\上学\结婚就是方法. * 面向过程:人出生.上学.工作. ...

  5. JavaScript从初见到热恋之深度讨论JavaScript中的面向对象。

    JavaScript中的面向对象.面向对象的三个基本特征:封装.继承.多态. 1.封装 js的封装如下 定义Person类 function Person(name,age,sex) { this.n ...

  6. 如何理解并学习javascript中的面向对象(OOP) [转]

    如果你想让你的javascript代码变得更加优美,性能更加卓越.或者,你想像jQuery的作者一样,写出属于自己优秀的类库(哪怕是基于 jquery的插件).那么,你请务必要学习javascript ...

  7. 前端开发:javascript中的面向对象

    前端开发:面向对象与javascript中的面向对象实现(一) 面向对象理解: 面向对象是一种对现实世界理解和抽象的方法,是一种先进的程序设计理念,是一种比较抽象的,多形态的设计模式.我们可以这么理解 ...

  8. 深入理解javascript中实现面向对象编程方法

    介绍Javascript中面向对象编程思想之前,需要对以下几个概念有了解: 1. 浅拷贝和深拷贝:程序在运行过程中使用的变量有在栈上的变量和在堆上的变量,在对象或者变量的赋值操作过程中,大多数情况先是 ...

  9. 正确理解javascript当中的面向对象

    认识面向对象: 为了说明 JavaScript 是一门彻底的面向对象的语言,首先有必要从面向对象的概念着手 , 探讨一下面向对象中的几个概念: 1.万物皆为空:万物皆对象 2.对象具有封装和继承特性 ...

随机推荐

  1. mySQL的安装和基础使用及语法教程

    mySQL的安装和基础使用及语法指南 一.MySQL的安装.配置及卸载 1.安装 2.配置 3.mySQL5.1的完全卸载 4.MYSQL环境变量的配置 二.MySQL控制台doc窗口的操作命令 1. ...

  2. 剑指Offer-构建乘积数组

    package Array; import sun.security.util.Length; /** * 构建乘积数组 * 给定一个数组A[0,1,...,n-1],请构建一个数组B[0,1,... ...

  3. 集合详解(python)

    集合概念 集合是一个数学概念:由一个或多个确定的元素所构成的整体叫做集合. 集合中的元素三个特征: 确定性(元素必须可hash) 互异性(去重)--将一个列表变为集合,就自动去重了 无序性(集合中的元 ...

  4. 笔记:Eclipse 安装 m2eclipse 插件

    M2eclipse 插件 Eclipse 下一款十分强大的 Maven 插件,可以访问 http://m2eclipse.sonatype.org 了解更多该项目的信息,如果需要安装该插件可以按照如下 ...

  5. 最小化安装CentOS7的网卡设置

    实验环境:CentOS 7 Minimal Installation 64bit (1511) 最小化安装CentOS 7 后,查看网卡的信息让人很意外,因为网卡的命名规则变了,网卡的名字让人很难懂. ...

  6. 【Linux】 linux中的进程信息相关的一些内容

    _ linux进程信息 ■ top top命令用于动态地查看系统的进程和其他一些资源的信息.开启top的时候可以加上-t <sec>来设置top更新的频率高低.进入top界面之后,可以输入 ...

  7. 《PHP 设计模式》翻译完毕

    翻译进度请见:https://laravel-china.org/docs/php-design-patterns/2018?mode=sections 设计模式不仅代表着更快开发健壮软件的有用方法, ...

  8. Lucene教程 -------(一、初始Lucene)

    一.lucene的介绍 lucene是一个全文检索的框架,apache组织提供了一个用java实现的全文检索的开源项目.功能非常的强大,api非常简单,并且有了全文检索的功能支持可以非常方便的实现根据 ...

  9. heartbeat错误排查

    错误一: [root@snale2 ha.d ::]#service heartbeat start Starting High-Availability services: INFO: Resour ...

  10. Dynamics 365 for CRM: Sitemap站点图的可视化编辑功能

    Dynamics 365 for CRM 提供了Sitemap站点图的可视化编辑功能 在之前的所有版本中,我们只能通过从系统中导出站点图的XML进行编辑后再导入(容易出错),或使用第三方的Sitema ...