在ES6出现之前,JavaScript不能真正被称为 面向对象的编程语言,因为 class
仅仅作为其保留字而非关键字,而ES6之后,引入了class
,使程序员可以用自己更加熟悉的方式创建对象;
至于ES6和ES5有什么区别,应该就是上面提到的可以让程序员更爽地coding,而程序员爽了,根据 工作量守恒定律,总有某个事物要干更多的活,没错,就是计算机。
为了兼容某些不支持ES6的浏览器,我们可以引入 Babel 库将ES6代码 “编译” 成ES5之后执行,而对于支持ES6的浏览器,在其JavaScript引擎中会自动进行“编译”操作;
所以,总体上看,ES6和ES5在功能上是等效的,即用ES6能完成的任务,用ES5必然能够完成,只是在语法上,ES6提供了更多的 语法糖,让程序员尝到甜头;所以为了更好的理解JavaScript对象,我们回归初心,从ES5中窥视JavaScript所创建的对象世界;
对象的创建
对象有两个基本元素:属性和方法;
属性用于存储数据,方法用于存储代码;接下来,我们从简单到复杂,来理解JavaScript创建对象的演变史。
创建Object对象并赋值时代
我们可以通过 new Object
创建Object,并为其赋属性和方法来创建对象:
// 代码段 1
var p = new Object();
p.name = "x";
p.age = 20;
p.say = function(){
console.log("My name is",this.name,",this year is",this.age);
}
这样我们就得到一个含有2个属性,1个方法的对象;
通过执行 p.say()
得到输出 : My name is x ,this year is 20
;
但是,这些几行代码非常松散,每一行代码都像一条单独的语句,为了体现这些属性和方法是一个整体,而不是一个个独立的存在,我们需要进入下一个时代;
字面量赋值时代
我们通过给一个变量赋一个字面量,即可创建一个对象:
// 代码段 2
var p = {
name : "x" ,
age : 20 ,
say : function(){
console.log("My name is",this.name,",this year is",this.age);
}
}
这样,我们就创建好了一个对象,这个对象有了自己的属性和方法;
通过 console.log(typeof p)
,输出为object
可知, p 的类型是一个对象;
代码段1和代码段2相比,代码段2的结构更为合理,所有的属性和方法都用大括号包含,更利于阅读,也体现了整体性,但功能上是等效的;
不过我们又发现,每次要创建一个相似的对象,都需要写一遍属性名,非常地不优雅,所以,我们又要进入下一个时代;
工厂模式时代
我们可以通过调用一个函数创建我们需要的对象,并返回该对象,从而使代码可重用,这个函数就是传说中的 工厂,代码如下:
// 代码 3
function createPerson(name,age){
var t = new Object();
t.name = name;
t.age = age;
t.say = function(){
console.log("My name is",this.name,",this year is",this.age);
}
return t;
}
var p = createPerson("x",20);
代码3是将代码1变为了工厂模式;
下面再将代码2也变为工厂模式:
function createPerson(name,age){
return {
name:name,
age : age,
say = function(){
console.log("My name is",this.name,",this year is",this.age);
}
};
}
var p = createPerson("x",20);
```
工厂模式的代码要比前面两个时代的代码优雅,我们只需要调用一个函数即可获得我们想要的对象;
但是,我们又发现了一个新的问题(别问我为什么总是能发现新问题,因为就是有一双善于观察的眼睛,手动傲娇 ^_^):
通过上述3中方式创建的对象在使用 `typeof` 时,返回的都是 `object`,而通过 `实例 instanceof 类` 只有在类为 `Object`时,才返回true,就是说,上面3中方法创建的对象都是无差别的对象,我们不能分辨出它们的类型;
这就麻烦了,比如我们有这么一个函数:
```javascript
function seeDoctor( o ){
if(o是人){
请人医治疗
}
if(o是动物){
请兽医治疗
}
}
```
那我们创建的对象因为不能判断其是人是兽,将不能选择适合的治疗方案;
要解决这个问题,有两种思路:
- 给对象添加信息,即为每一个对象添加一个属性 `type` ,用于指明其类型;
- 让js解释器能够判断其类型;
第一种方式比较 *丑陋*,我们需要管理更多的数据,但比较容易理解;第二种是更优雅的方法,也推动我们进入下一个时代;
### 构造函数时代
构造函数时代的主要任务是让创建的对象自带类别说明属性,即通过`instanceof` 就能判断出其所属的类:
```javascript
// 类是一个函数,约定:
// 普通函数第一个字母小写,类函数第一个字母大写
function Person(name,age){
this.name = name;
this.age = age;
this.say = function(){
console.log("My name is",this.name,",this year is",this.age);
}
}
function Animal(){}
var p = new Person("x",20) ;
```
注意,创建对象时,必须使用关键字 **new** 。
此时,我们通过 `p instanceof Person`,返回的结果为 `true`,而通过 `p instanceof Animal` ,返回结果为 `false`,从而使对象实例自带类型属性;
完美! But,又双叒叕发现了不足,我们用上述各种函数创建两个对象:
```javascript
var p1 = new Person("x",20);
var p2 = new Person("y",21);
console.log(p1.say==p2.say) ; // 输出的是false
```
我们发现:虽然 *say* 函数的代码相同,但两个对象实例的 *say* 居然指向不同的代码块,如果我们有100个实例,相同的代码块就需要有100份,极大的内存浪费,这是我们所不能忍受的,因此迫切希望下一个时代的到来!
### 构造函数+原型时代
原型就是所有对象实例所共享的一个 **对象**,这个对象中的属性就是共享属性(在c++中称为静态变量),方法就是共享方法;
```javascript
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.say = function(){
console.log("My name is",this.name,",this year is",this.age);
}
var p = new Person("x",20);
```
上述代码创建的对象实例 p 有自己的属性name和age,以及共享的方法say;通过 `p.say()` 即可打印出:*My name is x ,this year is 20* ;
执行 `p.say()` 的时候,p先搜索其自身是否有方法 `say`,如果有,就执行,如果没有,就搜索其原型对象是否有 `say` 方法,如果还是没有,就搜索其原型对象的原型对象是否有say属性(即沿着原型链搜索say方法,这也是继承的实现机制),如果原型链上都没有say方法,就抛出错误,否则,执行搜索到的方法。
p通过属性`p.__proto__` 指向原型对象 `Person.prototype` , 从而获取原型上的所有属性和方法;
如果p上也定义一个方法 `say`:
```javascript
p.say = function(){console.log("Hello,world");}
```
则该方法将会 **覆盖** 原型上的say方法,即调用 `p.say` 输出的将是 *Hello,world* ,而如果通过 `delete p.say` 删除掉 `say` 属性,则调用 `p.say` 时,执行的代码又是原型上的 say 代码;
总结:原型就是一个类所创建的所有对象实例共享的一个对象;
但是,原型对象的定义和构造函数分开了,这又使结构不太优美,所以,我们又得进入下一个时代;
### 构造原型时代
为了解决原型定义和构造函数分离的问题,我们决定将原型定义放到构造函数中,就出现了以下代码:
```javascript
function Person(name,age){
this.name = name;
this.age = age;
Person.prototype.say = function(){
console.log("My name is",this.name,",this year is",this.age);
}
}
var p = new Person("x",20);
```
OK,完成了原型定义和构造函数的合并,结构也变得更加优美了,但是,又出现了一个问题:
每次执行创建 Person 对象实例的时候,都要重新定义一遍 `Person.prototype.say` 方法,虽然这不会增加内存泄漏(以前定义的say代码由于没有被引用,内存块将会被自动回收),但却增加了cpu的工作量,所以我们需要进入下一个时代;
### 优化构造原型时代
为了避免 `Person.prototype.say` 函数的重复定义,我们可以先判断该函数是否已定义,如果没有定义,再对其进行定义:
```javascript
function Person(name,age){
this.name = name;
this.age = age;
if(typeof(Person.prototype.say)=="undefined"){
Person.prototype.say = function(){
console.log("My name is",this.name,",this year is",this.age);
} ;
}
}
var p = new Person("x",20);
```
通过以上7个时代的迭代,我们终于在 JavaScript中创建了一个基本上符合我们要求的对象;
完!