和少妇白洁一块儿学JavaScript

我不肯定JavaScript语言是否应该被称为Object-Oriented,由于Object Oriented是一组语言特性、编程模式、和设计与工程方法的笼统称谓,没有一个详尽和你们都承认的checklist去比较,就很难在主观意见上互相认同。node

但JavaScript百分之一百是一门Object语言。程序员

这句话有两个直接含义:shell

  1. 除了原始类型(primitive type)值以外,一切皆对象,包括函数;编程

  2. 一切对象都是构造出来的,有一个函数做为它的构造函数(constructor);浏览器

JavaScript的另外一个标志性特性是原型重用(prototype-based reuse),我在这里故意避免使用继承(inheritance)这个词语,是不想让读者马上联想C++/Java语言的继承,请忘记它们;网络

JavaScript里的对象并不是是Class的实例化,它没有静态结构的概念;固然这不意味这对象没有结构,但对象的结构只能由构造函数在运行时构造出来,所以构造函数在JavaScript里的地位是很高的,它是惟一负责结构的地方。数据结构

  1. 每一个对象都有一个原型,对象可使用和重载原型对象上的数据成员或方法,这是对象的惟一重用机制;闭包

介绍原型概念的文章和书不少,假定你理解原型的基本概念;这里须要指出的问题是,对象之间的属性重用,和面向对象里面说的重用是两回事;编程语言

你能够从重用的如此简单的定义看出,它惟一的设计目的是想减小对象的数量,它提供的机制就是让多个对象共享原型对象上的属性,同时又能够有重载能力;函数

但不要对此浮想连篇,它和Java语言里经过继承重用静态结构和行为是彻底两回事,即便说“JavaScript的原型化重用仅仅是行为重用,而Java的重用是结构和行为的双重重用”,这样的表述也没有意义,由于前者在运行时对象之间发生后者在静态编译时发生,一个在说咱们发明了活字印刷术让印刷变得更容易,另外一个在说咱们发明了电脑上的字体,你须要显示哪一个字就来到我这里拿;虽然结果有时看起来很像,可是机制上彻底风马牛不相及,不要上了阮一峰老师的当。

前面写的这三条,能够做为构造JavaScript对象系统的三个基础假设;

在JavaScript里最最底层的概念,并不是你在如何使用JavaScript语言的那些教材中看到的种种编程概念,而是两个词语:构造原型(或者说结构与重用)。

每一个对象必有构造函数和原型,整个JavaScript系统里你看到的全部东西,均可以在概念或模型上这样去理解,虽然实现上是另外一回事。

JavaScript对运行环境(runtime)的假设只有一个,就是单线程事件模型,其余关于虚拟机该怎样实现并没有定义,也没有bytecode的定义;ECMA262采用了一种相似伪码的方式定义了对对象、属性、函数的基本操做逻辑,全部实现,解释器也好,JIT也好,不管如何执行JavaScript脚本,只要保证语义一致便可;其实这种伪码定义方式自己,就暗示了某种特性,但咱们暂且不表。

单线程的事件模型不是万能的,但绝大多数状况下让编程变得简单;缺少runtime定义使得这门语言并不实用,开发者老是须要完整的东西,但好在JavaScript自诞生起就有了第一个runtime:网络浏览器,这让它有了立锥之地,以后又出现Node.js,它又找到一个能够生存的地方。

扯远了,咱们说回构造和原型的问题。

创世纪

假现在天咱们冒充上帝,开始构造JavaScript的对象世界,在这个世界里没有什么不是对象,也遵循前述原则;

咱们开始犯愁的第一个问题,彷佛咱们掉进了鸡生蛋蛋生鸡的逻辑怪圈。

对吧,第一个对象造不出来,由于对象须要构造函数构造,而函数也是对象,因此咱们前面说的那个对象必然不是第一个对象。

固然逻辑是逻辑,咱们能够先捏几个最原始的对象出来,而后把constructor__proto__引用装载上去,让它们成为系统最初的亚当和夏娃。反正上帝原本也回答不了亚当的妈是谁的问题,咱们也这么作。

最初在ECMA262里并无约定JavaScript实现必须提供能访问每一个对象的原型对象的方法,它只是一个概念;可是node/v8和js shell都提供了__proto__这个名字的属性,能够给出任何对象的原型;另外一个方法是使用Object.getPrototypeOf方法。

注意__proto__和function对象的prototype属性是两回事,prototype是function对象的特有属性(就像Array对象有length这个特有属性),__proto__才是对象的原型;下面的描述和代码里都使用__proto__这个很别扭的名字指对象的原型,它没歧义,和代码一致,再发明一个名字只会制造更多的混乱。

如今打开node shell。

> let m = {}
undefined
> m.__proto__
{}
> m.__proto__ === m
false

咱们建立了一个空对象,叫作m,它的原型也是一个空对象,虽然同为空对象可是它们并不是一个对象,因此并不相等;

> m.__proto__.__proto__
null
> let op = m.__proto__
undefined

再沿着原型链往上爬,看看原型的原型是谁?没了。这很好,咱们知道m的原型没有原型了,咱们先把m的原型叫作op

谁构造的op呢?

> op.constructor
[Function: Object]
> op.constructor === Object
true

op的构造函数是全局那个叫Object的对象,它自己是一个函数;不要把Object理解成namespace,或者把Object对象上的方法理解为“静态方法”,Object就是一个对象,它被赋值给了全局对象的Object属性,虽然它有特别的功能,可是要把它理解成咱们正在构造的对象世界中的一员,它只是在对象世界开天辟地时被构造好了而已,而咱们在讨论的就是这个构造的过程。

咱们已经回答了op的构造函数和原型都是谁的问题,如今牵扯出来一个Object,咱们继续检查;

> Object.constructor
[Function: Function]
> Object.constructor === Function
true
> Object.__proto__
[Function]

Object的构造函数是全局对象上属性叫Function的对象;Object的原型是个匿名函数,按照JavaScript关于构造函数的约定,它应该是构造函数的prototype属性:

> Object.__proto__ === Function.prototype
true
> let fp = Function.prototype
undefined

咱们给这个对象起个名字,叫fp。

> fp
[Function]
> fp.constructor
[Function: Function]
> fp.constructor === Function
true
> fp.__proto__
{}
> fp.__proto__.__proto__
null
> fp.__proto__ === op
true

这个fp也不是很麻烦,咱们发现它是一个匿名函数,它的构造函数是Function,而它的原型是op

最后来看Function

> Function.constructor
[Function: Function]
> Function.__proto__
[Function]
> Function.__proto__ === fp
true

Function本身耍了一个赖皮,本身是本身的构造函数因此解决了鸡和蛋的问题。Function的原型和prototype属性指向了同一个对象fp

因此到此为止呢,咱们扒开了JavaScript世界里最原始的几个对象,他们的原型关系是:

Function and Object -> fp -> op -> null

至于构造函数呢,由于Object是function,它的prototype是op,按照JavaScript的约定:function对象的prototype属性指向的对象应该把constructor属性设置成该function对象,即:

functionObject.prototype.constructor = functionObject

一样的道理,Function的prototype是fpfp的constructor也要设置成Function

这是JavaScript里最基础的四个对象;其余的一切对象,在模型和概念中均可以构造出来;

若是你在写一个解释器,你在最初就要把这些东西创造出来,而后创造一个global对象(或者叫context),在这个对象上装上ObjectFunction,让他们成为全局对象,至于opfp,就让他们藏在里面好了;编程中没有须要用到他们的地方,若是要找到他们,能够用Object.prototype或者Function.prototype来找到。

因此到此为止,咱们启动了JavaScript的对象世界,有了Function咱们就能够构造函数对象,有了函数咱们就能够构造更多的对象,若是语言上容许(即不须要经过native code实现特殊功能),咱们能够继续建立Object.prototypeFunction.prototype上的那些函数对象并把他们装载上去,在概念模型上,内置对象没有什么了不得,他们仍然能够被理解成被构造出来的对象;

事实上全部的函数做用域和函数内的变量也能够被理解成对象和它的属性,在本文的结尾咱们会谈这个问题,固然它只是模型上的;

咱们阐述了一切皆对象的含义;这个对象模型够简单吗?我认为是的;它只有对象,函数,原型三个概念。

一些人说JavaScript是Lisp穿了马甲,从对象模型上是能够成立的;由于Lisp里的数据结构是List,它是一个链表,每一个节点有两个slot,一个用于装载值,另外一个装载next;而JavaScript对象其实也是链表,只不过它给每一个节点增长了一个字符串标签,即所谓的property name;但若是你用for ... in语法遍历对象内部的时候,你仍然能看到内部结构的顺序是稳定的,仍然是链表;

给每一个节点加上label是JavaScript设计上很是聪明的地方,由于它让文科生也能够参与如火如荼的编程活动。

可是这个对象模型说完了好像什么也没有说?怎么JavaScript书上讲的那么多概念都没有提到呢?

这是问题的本质,也是不少Java过来的程序员很费劲的地方;JavaScript利用上述的这个很是简单的对象模型,去模拟,或者说实现,其余全部的编程概念。

JavaScript最初的设计目的只是用于很是简单的一些小功能,须要可编程;无论Brenden Eich是天才、拙劣、仍是巧合的模仿了Lisp,以及Smalltalk和Self,他把两个很是简单且独一无二的事情结合在了一块儿:

Lisp是λ Calculus在编程语言上的直接实现;原型重用的意思则是:

JavaScript:让咱们消灭必须用静态定义约定动态对象结构的作法吧,编程君!任何静态能定义出来的结构,咱们在运行时也能够经过不断的复制得到啊,只是会慢一点点而已。
编程君:内存不够怎么办?
JavaScript:咱们有原型啊!
编程君:好吧,但你要请我吃冰激凌。

不谈工程实现,仅仅在概念和模型上纸上谈兵的话,JavaScript语言模型之简单,是不少老牌语言和新兴脚本语言都难以企及的,它很是纯粹。

函数对象与构造函数

在谈构造函数以前咱们先看一段代码:

// 构造对象的方式1
const factory = (a, b) => {

  return {
    a: a,
    b: b,
    sum: function() {
      return this.a + this.b
    }
  }
}

return语句后面返回的对象,被称为ex nihilo对象,拉丁语,out of nothing的意思,即这个对象没有用一个专门的构造函数去构造,而是用那个全局的Object去构造了。

若是你仅仅是想建立具备一样结构的对象实现功能,这样的工厂方法足够了。可是这样写,一方面,重用不方便;另外一方面,若是我只构造几十个这样对象,可能不是什么大问题,可是若是要构造一百万个呢?构造一百万个会引起什么问题?

让咱们来从新强调对象的另外一个含义:对象是有生命周期的;由于函数也是对象,因此函数对象也不例外;这一点是JavaScript和Java的巨大差别,后者的函数,本质上是静态存在的,或者说和程序的生命周期一致。但JavaScript里的函数对象并不是如此。

前面的sum属性对应的匿名函数对象,它是何时建立呢?在return语句触发Object构造的时候。若是要建立一百万个对象呢?这个函数对象也会被建立一百万次,产生一百万个函数对象实例!

换句话说,这个工厂方法建立的一百万个对象不只状态各有一份,方法也各有一份,前者是咱们的意图,但后者是巨大的负担,虽然运行环境不会真的蠢到去把代码复制一百万份,但函数对象确实存在那么多,对象再小也有基础的内存消耗,数量多时内存消耗无论怎样都会可观的,若是对象具备不仅一个函数,那浪费就更可观了。

这是JavaScript的一切皆对象,包括函数也是对象的代价。

遇到这样的问题通常有两种办法,一种是修改机制,即前面说的模型,引入新的概念;另外一种是加入策略,即在语言实现层面增长约定,可是利用现有机制,不增长概念;

JavaScript的设计者选择了后者,这也是JavaScript的看似古怪的构造函数的由来。

设计者说能够这样来解决问题:若是一个函数对象的目的是构造其余对象(即构造函数),它须要一个对象做为它的合做者,装载全部被构造的对象的公用函数,二者之间的联系这样创建:

  1. 构造函数对象须要具备一个名称为prototype的属性,指向公用函数容器对象;

  2. 公用函数容器对象须要具备一个名称为constructor的属性,指向构造函数对象;

这个公用函数容器对象在建立function对象的时候,若是不是arrow function,它自动就有prototype属性,指向一个空对象;若是是arrow函数,没有这个属性,arrow函数也不能够和new一块儿使用;

> function x() {}
undefined
> x.prototype
x {}
> const y = () => {}
undefined
> y.prototype
undefined
>

当调用构造函数时,经过使用new关键字明确表示要构造对象,这时函数的工做方式变了:

  1. 先建立一个空对象N,把它的原型__proto__设置成该构造函数对象的prototype属性;

  2. 把N的constructor属性设置为构造函数对象;

  3. 把N bind成构造函数的this;

  4. 运行构造函数;

  5. 返回新对象N,无论构造函数返回了什么;

new被定义成关键字是为了兼容其余语言使用者的习惯,写成函数也同样:

function NEW(constructor, ...args) {
  let obj = Object.create(constructor.prototype)
  obj.construtor = constructor
  constructor.bind(obj)(...args)
  return obj
}

另外一个关键字instanceof,则反过来工做,若是表达式是A instanceof B,若是不考虑继承问题,就去判断A.constructor === B便可;继承的问题后面讨论。

理解了这个过程就会明白,JavaScript里的构造函数问题,其实并不是在发明构造函数的新语法,而是保持语言模型不变,让他可以构造共享原型的对象的一种方式。

这就是为何在ES5语法里看到的构造函数和它的原型的代码是相似这样的:

function X(name) {  this.name = name }

X.prototype.hello = function() { console.log('hello ' + this.name) }

var x1 = new X('alice')
x1.hello()

var x2 = new X('bob')
x2.hello()

但即便须要这样作,上面的写法也不是惟一的写法,也能够这样直接写工厂方法:

let methods = {
  hello: function() {
    console.log('hello' + this.name)
  }
}

function createX(name) {
  let obj = Object.create(Object.assign({}, methods)) // 使用Object.assign能够merge多个methods
  obj.name = name
  return obj
}

一样实现构造共享原型的对象,只是返回的对象不具备constructor属性,instanceof无法用,但若是你不须要instanceof,也不须要设计多层的继承,这是可用的方法;

总结一下关于构造函数的这一节;

首先JavaScript在定义函数时,并不区分这个函数是否是构造函数,是不是构造函数取决于你是否使用new调用;

其次,若是一个函数是构造函数,它不是一我的在战斗,它须要和它的prototype属性指向的对象合做,该对象将是构造的对象的原型,请把两个对象而不是一个对象印在脑子里,这对后面理解继承很是关键;

第三,和Java里那种数据成员和方法成员在心理上位于一个对象容器内不一样,JavaScript的对象在设计上就要理解为数据(或者状态)在本身身上,方法(函数对象)在原型身上,这仍然是两个对象在合做,表现得象一个对象

继承

JavaScript里的继承仍然不是语言特性,在这个问题上咱们继续沿用前面的思路:用JavaScript的原型重用能力,去模拟,或者说实现Java语言里的继承形式

咱们先说思路,假想咱们就是Brenden Eich几分钟。

假如咱们已经用构造共享原型的对象的思路,写了一个构造函数BaseConstructor,它负责建立每一个对象的数据或状态属性,也有了一个合做者BaseConstructor.prototype,它提供了方法BaseMethod1, ...;如今咱们须要拓展它,要增长一部分状态或者属性,也要增长一部分方法,咱们该怎么作?

首先咱们考虑拓展方法,这不难,若是咱们构建一个对象,把它的原型设置为BaseConstructor.prototype,而后在新对象里添加方法便可;

其次咱们将来须要使用的对象应该都以该对象为原型,由于原有方法和扩展方法都能经过它访问;这预示了咱们须要一个新的构造函数以该对象做为prototype属性;逻辑上能够是这样:

Base     <-> Base.prototype
  ^            ^
  |            *
  | call       * __proto__
  |            *
Extended <-> Extended.prototype

Extended函数能够建立Extended.prototype里扩展方法所须要的状态或数据成员;可是Base.prototype里须要的状态或者数据成员须要Base来建立,咱们确定不但愿把Base里的代码复制一份到Extended内;咱们须要调用它来建立原有方法所需的状态或数据成员。

function Base(name) { this.name = name}
Base.prototype.printName = function() { console.log(this.name) }

function Extended(name, age) {
  Base.bind(this)(name)
  this.age = age
}
Extended.prototype = Object.create(Base.prototype)
Extended.prototype.constructor = Extended
Extended.prototype.printAge = function() { console.log(this.age) }

这里tricky的地方有几处:

第一,在Extended函数内,先把this bind到Base构造函数上,而后提供name参数调用它,这样this就会具备printName所需的name属性,实现结构继承;

第二,咱们使用Object.create方法建立了一个以Base.prototype为原型的新对象,把它设置为Extended.prototype,实现行为继承;

第三,把Extended.prototype.constructor设置为Extended构造函数,这样咱们可使用instanceof语法糖;

最后咱们在Extended函数内建立新的状态或数据属性,咱们也在Extended.prototype上添加新的函数方法;

或者咱们说咱们找到了一种方式既拓展了构造函数构造的新对象的数据属性,也拓展了它的函数属性,沿着两条链平行实施,达到了咱们的目的。

在JavaScript里使用这种在原有构造函数及其prototype对象上拓展出一对新的构造函数和prototype对象的拓展方式,咱们称之为继承。

由于对象能够重载原型对象的属性,因此在function.prototype的原型链上,重载函数的能力也具备了。

Class

JavaScript里没有type系统意义上的Class的概念。class关键字仍然是语法糖。

class A {

  constructor () { // 这是构造函数
  }

  method() { // 这是A.prototype上的方法
  }
}

这个语法比前面分开写构造函数和prototype对象的写法要简洁干净不少,可是带着Java的Class的概念试图去理解它,更容易被误导了。

A在这里仍然是函数对象,只不过它只能当构造函数用,必须用new调用;其余还有一些细节差别,不赘述了;

若是是继承呢?

class Base {
  constructor() {}
  method1() {}
}

class Extended extends Base {
  constructor() {
    super()
    //...
  }
  method2() {}
}

也是大同小异;Extended构造函数内须要调用super()来实现调用Base构造函数构造属性;这一句必须调用,不然没有this,这是class语法和前面ES5语法的一个差别,在ES5语法内,新对象是在调用Extended构造函数时马上建立的,在class语法中,这个对象是沿着super()向上爬到最顶层构造函数才建立的,因此若是不调用super就没this了。

实际上在JavaScript里的继承,应该看成一种Pattern来理解,即:使用构造函数和它的prototype属性对象合做来模拟传统OO语言里的继承形式,把它叫作Inheritance Pattern恰当的多。

函数做用域

前面咱们曾冒充上帝,假想一个JavaScript程序启动后,如何从零开始构造整个对象世界;如今咱们得寸进尺,冒充上帝他妈,考虑站在执行器的视角上,若是拿到一份JavaScript脚本如何执行;

假定咱们已经使用了底层语言,例如C/C++,实现了JavaScript的对象模型,即很容易建立对象,维护原型链。

咱们先建立一个空对象,把它称为global,先把标准的内置对象都做为全局变量名称装载进去;而后开始运行。

JavaScript是个单线程模型,因此假定咱们用栈的方式来实现计算;基本操做符和表达式的栈计算就很少说了,咱们只说遇到函数怎么办。

通常来讲遇到函数应该约定在栈上处理参数和返回值的方式,但这个可有可无,有关紧要的问题是咱们须要把传统的Function Frame的概念,即对一个函数在栈上分配局部变量的概念,换个思惟,咱们不用Function Frame,而是建立一个空对象来表式一个Function Frame,咱们一行一行的读入代码,遇到局部变量声明就在这个对象上装上一个属性,遇到修改局部变量的时候就给它赋值;

若是这样作,咱们就能够把Function Scope(通常说Function Scope指的是代码层面的Lexical Scope,这里咱们把Function Scope和Function Frame混用)做为原型链串起来,词法域中外围的Function Scope是原型,内部的Function Scope是对象;这样Function Scope的引用可能出如今栈上,但它自己并不是分配在栈上;Function Scope对象的建立是在调用函数时,它的销毁咱们能够暂时期望垃圾回收器,可回收的时间是该函数已经完成执行且没有其余Function Scope引用该Scope;

若是你仔细观察在Function Scope构成的链上查找变量名(Identifier)的时候,其逻辑和在原型链上查找属性的方式如出一辙;用这样的方式也能够准确找到闭包变量,惟一的区别是这里须要小小的修改一下原型链的约定,原型上的属性能够直接修改,由于闭包变量是能够赋值的;

这就是前面咱们说Function Scope也能够看成是对象处理的缘由。

你能够想象出来这个解释器能够写得多小和多简单,并且若是没有hoisting,它能够在源文件还没下载完就开始投入运行,而不是一开始就把整个语法树都解析出来;

若是你问为何早期的JavaScript的var没有block scope支持,由于block scope按照这种思路来讲,须要为block scope单首创建对象。

因此在这个讨论里,你能对JavaScript最初呱呱坠地时的一些小想法得到一些感觉;它从一开始只想用一个使人震惊的简单的方法作几件简单的小事情,好比赚一个亿,但这并不说明它无能,相反,在数学和编程的世界里,越是简单的事情越有无穷无尽的能量。

写到这里,我想我说完了本身对JavaScript的一切皆对象的认知,欢迎探讨。

最后鸣谢少妇白洁愿意出如今本文题目中。

相关文章
相关标签/搜索