JavaScript之对象

在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中创建了一个基本上符合我们要求的对象; 

完!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 前言 这是读书笔记第二篇,看完之后突然发现自己对js的内置的一些东西还是了解的不够全面,很多方法见都没见过,啥用都...
    不止前端阅读 206评论 0 0
  • 中午,有个同事问我们需不需要买鞋,她说她领取了一家鞋店的优惠券,有很多张,都是满100减100的,优惠力度特别的大...
    辰苓阅读 1,188评论 0 2
  • copy和mutablecopy 源于对数据的复制需求,对于对象类型的数据来说,区别于直接持有这个数据对象的方式,...
    纵横而乐阅读 438评论 0 1
  • 职位描述 【岗位职责】 1、对房地产市场数据进行日常收集归类统计;及时跟踪和研究国家宏观经济政策的走向,对国家重大...
    没头没脑傻开心阅读 569评论 0 2
  • 小洋洋出生后,我与宝爸基本没有二人世界啦,有的都是三人世界。但这个周末的三人世界有点特别(∩_∩)。 上篇更文中宝...
    李梅树阅读 656评论 1 3