在JavaScript中,对象是最重要的概念,因为除了基本数据类型,其他的一切都是对象。为此,JavaScript提供了多样的创建对象的方法。同时,函数又是极为特殊的一种对象,因此,JavaScript针对函数也做了许多巧妙的使用优化。
六、在JavaScript中,创建对象的方法有多少?
在前面两篇文章中,已经涉及了三种创建对象的方法:
- new Object()
- Object.create()
- { }
三种方法都比较简便。在ES6中,针对大括号({ })语法创建对象进一步做了优化。
1、再探大括号法创建对象
优化一:函数属性的简写
以前,为对象定义函数要这样写:
var obj = {
add: function(a,b){
return a + b;
}
}
现在可以这样写了:
var obj = {
add(a,b){
return a + b;
}
}
第二种写法乍看是第一种写法的语法糖,然而二者却不完全等价,第二种写法中,函数内部也可以通过add来调用自身,因为第二种写法本质上是这样的:
var obj = {
add: function add(a,b){
return a + b;
}
}
function 后面的add仅限于在函数体内使用,这一点在该系列第一篇中已经讲明。
优化二:基本数据属性和对象属性的优化
假如以前你这样定义了一个对象:
var a = "foo",
b = 42,
c = {};
var o = {
a: a,
b: b,
c: c
};
那么现在,你可以这样写了:
var a = "foo",
b = 42,
c = {};
var o = { a, b, c };
这种写法就完全是一种语法糖了,简单讲,对于以 prop : prop 方式定义的属性,也就是属性名与代表属性值的变量名称相同,就可以直接写 prop。
优化三:属性名也可以用表达式
var i = 0;
var a = {
["foo" + ++i]: i,
["foo" + ++i]: i,
["foo" + ++i]: i
};
上述代码在创建对象时,JavaScript会首先把中括号([])中的内容当作表达式进行求值,求值的结果再转为字符串作为属性的名称。因此,对于对象a,我们有:
a.foo1 == 1; // true
a.foo2 == 2 ; // true
a.foo3 == 3 ; // true
2、用函数创建对象
本系列第一篇中,我们指出,函数是一种特殊的对象,最大的特殊之处在于它是可以被调用的(a object which can be called !),其基本的调用方法与Java是一致的。
实际上,任何一个函数还有一种特殊的调用方式,姑且称之为 new 调用,这种调用会创建一个对象返回。
举例如下:
function add(a,b){
var result = a + b ;
return result ;
}
这是一个再普通不过的一个函数,我们可以这样使用它:
var sum = add(5,9) ; //sum的值为14
然而,我们还可以这样使用它:
var obj = new add(5,9) ; // typeof obj == object
上面,我们仅仅是在正常的调用前面加了一个关键字 new ,整个函数的执行逻辑就完全发生了变化,最大的变化是它不再返回函数体内的return语句中的值,而是返回了一个对象!
我想,每一个从Java转来学JavaScript的人,看到这样的情况,都会觉得不可思议吧。
有没有感到在JavaScript的世界中,函数作为一个特殊的对象,似乎凌驾于普通对象之上了?这货竟然可以生成对象!
确实是这样的,如果你学习过JavaScript,你应该会听过一句话,在JavaScript中,函数是一等公民,说的就是函数的这种特殊性。
让我们沉下心来,看看上面的new调用到底是怎么执行的:
- JavaScript引擎执行到 new 调用所在的行时,它立马明白了,这里不是一个普通的函数调用,而是一个new调用,用户想要通过函数调用生成一个对象,于是JavaScript创建出来一个新的对象,姑且称其为obj。
- 然后,JavaScript引擎会将函数的prototype属性所指向的对象设为obj的原型。
实际上,每个函数都有一个prototype属性。当你用一个函数创建一个对象时,新建对象的原型会被自动设置为函数prototype属性指向的对象。 - 然后,JavaScript引擎会把关键字 this绑定到新创建的对象obj上,也就是说,之后在函数体内对this关键字的操作就是对新创建的对象obj的操作。
- 之后,JavaScript引擎会根据用户在new调用中传入的参数(本例中为5和9,不同的函数要求的参数也不相同,也可以没有参数)来一句一句执行函数, 如果函数最后没有return语句,那么当函数体执行完毕后,JavaScript会直接把对象obj返回给调用者。如果函数最后有return语句,JavaScript会判断一下return语句中的返回值是不是一个对象,如果是一个对象,那么就把这个对象返回给调用者,如果return语句返回的是一个基本数据类型,而不是一个对象,那么JavaScript仍然把对象obj返回给调用者。在本例中,JavaScript会首先执行语句
var result = a + b ;
然后遇到了return语句,JavaScript发现这个return语句返回的是一个基本数据类型,不是一个对象,JavaScript果断丢弃了result这个值,函数计算出来的result在new调用中完全没有任何用!JavaScript转而把函数开始执行时新建的对象obj返回给了我们。
注:这个对象obj和变量result真的是一点关系也没有!在本例中,整个函数体内我们没有对关键字this进行任何操作,所以对象obj一直没有发生什么变化。
观察上面创建对象的过程,我们发现有几点比较别扭:
- add函数首字母是小写,new add(5,9)形式上不够优美,在面向对象语言实践中,我们更习惯首字母大写的new调用。
- 语句
var result = a + b ;
对最后的对象没有什么影响,浪费CPU资源 - return语句最后返回一个数字,也没有什么用,还让JavaScript多了一次判断
于是,大家就约定(仅仅是一个约定,不是语言本身的要求):
- 当你创建一个专门用来生成对象的函数时,就把函数名字的首字母大写
- 函数体内只保留对最后生成对象有影响的语句,也就是对this有影响的语句
- 不要最后的return语句,确保this所代表的对象能够返回给用户
下面就是一个符合上面约定的例子:
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
你可以这样来使用Person函数:
var milter = new Person("milter", 31, "男");
进一步思考一个问题:上面代码中,我们创建了一个Person对象milter,但此时milter只具有三个属性,并不具有方法,也就是行为能力,那么如何为Person对象添加方法呢?
上面我们提到,当JavaScript看到我们用 new 调用一个函数时,它会首先创建一个对象,并将函数的prototype属性指向的对象作为新建对象的原型。因此,我们可以在Person.prototype中添加方法,这样,所有用函数Person创建的对象都会具有我们添加的方法。
例如,我们可以为Person添加一个run方法:
Person.prototype.run = function (){
// running
}```
那么,所有用Person函数创建的对象现在都具有了run方法,如:
milter.run() ;
神奇的是,当我们改变Person.prototype时,那些在改变之前用Person创建的对象也会随之改变。因为所有用Person创建的对象的原型是同一个Person.prototype对象。这是体现JavaScript动态性的典型例子。
现在我们知道,用Person创建的对象的原型是Person.prototype,那么,问题来了,Person的原型又是什么呢?
答案是:所有的函数的原型都是一个特殊的对象:**Function.prototype**,该对象中包含了作为一个函数的普遍属性和方法,而Function.prototype的原型又是Object.prototype。所以,Person的原型链是:
**Person --->Function.prototype---->Object.prototype**
而用Person创建的对象的原型链是:
**milter--->Person.prototype---->Object.prototype**
也就是说,函数的原型和函数创建的对象的原型不是一回事,一定要搞清楚,初学者很容易将二者混为一谈。
明白了这一点,我们也就知道:
Person.eat = function (){
//eating
}
为Person添加的方法eat并不会被用Person创建的对象所继承,它属于Person函数本身的方法。
只要使用JavaScript的人都遵守上面的约定,那么,每当你看到一个名字首字母大写的函数,你就应当立即反应过来,这个函数是用来创建对象的,你应当用**new调用**来使用它,而不是将它当作普通函数使用。
##3、用class创建对象
看到class,你可能会以为JavaScript中也引入了类的概念,然而实际情况可能会让你失望,用class创建对象,其本质还是用函数创建对象,只不过是包装成了类的形式而已。一个class其实就是一个函数。
我们知道,创建函数有两种方式:声明的方式和表达式的方式,这一点在本系列第1篇中有具体的分析。和创建函数一样,也有两种方法创建一个class,也是声明的方式和表达式的方式,如下所示:
//声明的方式创建class
class Person {
constructor(name, age,sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
};
上述代码以**声明的方式**定义了一个class Person。此时,创建一个Person对象的方法是:
var person = new Person('milter',31,'男');
我们发现,这个class Person和上一小节中的函数Person非常像,二者创建对象的方法甚至完全一样!最明显的不一样的地方是class Person中多了一个方法 constructor,这个方法在每个class中**有且只有唯一的一个**,其作用是初始化用该类创建的对象。
从本质上看, class Person就是函数Person的一个**语法糖**,它使得定义一个创建对象的函数更加容易,语义更加清晰,对我们这些从Java转过来的程序员更加友好。除此之外,这里没有任何神秘的东西。
如果你测试一下 class Person 的类型,像这样
typeof Person
你会得到结果 **function** 。说明class Person 本质上就是一个函数。
但是class Person与函数Person有一点小差别。以声明形式定义的函数Person可以在函数定义之前使用,但是,以声明形式定义的class Person,却必须在定义之后才能使用,这点需要在使用中注意。
我们知道,对于函数Person,可以用表达式的方式定义,如下所示:
var Person = function (name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
同理,class Person也可以用表达式的方式定义如下:
//表达式的方式创建class
var Person = class {
constructor(name, age,sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
};
在用表达式方式定义函数时,你可以在function 后面添加一个名字,以便在函数体内使用该函数,同样,你也可以在class 后面添加一个名字,以便在class内部使用该class。
在用函数语法创建对象中(参见上一小节),为对象添加**共同的**方法和属性需要在Person.prototype中添加,为函数Person本身添加方法和属性需要直接在Person中添加。利用class语法创建对象对此做了很大的优化,请看如下代码:
//声明的方式创建class
class Person {
constructor(name, age,sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
run(){
//running
}
static sextype(){
return ['男','女'] ;
}
};
上述代码中,我们进一步为Person创建了两个方法,一个是普通方法run,一个是static方法sextype(请注意:各个方法之间既无逗号分隔,也无分号分隔)。二者有什么区别呢?
普通方法将会被设置成Person.prototype的方法,static方法将会被设置成Person本身的方法。如下图所示:
|Person|Person.prototype|
| :-------------: |:-------------:|
| sextype | run |
由此,我们知道,sextype是不会被class Person创建的对象继承的,而只能通过Person.sextype的方法调用。run会被class Person创建的对象继承。
可以看到,当我们将class Person还原成它的本质**函数**后,我们就能明白class中的static方法和普通方法的区别,也很容易理解为什么在对象中没法调用static方法。
好奇的你可能会问,那constructor方法被放到哪里了呢?答案是:Person.prototype,它会被所有的对象继承。
但是由于这个方法的特殊性,JavaScript不允许我们直接调用它。也就是说,你不能这样调用:
`milter.constructor('lucy',18,'女'); //语法错误`
class 语法还允许我们使用extends和super关键字,来看一个例子:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return this.x+':'+this.y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y);
this.color = color;
}
toString() {
return super.toString() + ' in ' + this.color;
}
}
**友情提示:**Point 和ColorPoint本质上都是函数。
extends关键字在这里主要有三个作用:
- 将ColorPoint的原型设为 Point,所以此时ColorPoint的原型链成了:
**ColorPoint --->Point---->Function.prototype---->Object.prototype**
由此我们可以知道,ColorPoint将会继承Point中的静态方法。
- 将ColorPoint.prototype的原型设为Point.prototype,所以此时ColorPoint.prototype的原型链成了:
**ColorPoint.prototype---->Point.prototype---->Object.prototype**
由此我们知道,用ColorPoint创建的对象将会继承Point中的普通方法。
- 强制ColorPoint在其constructor方法中调用Point的constructor方法,调用语法为:
super();
另一个使用super关键字的地方是:在ColorPoint的toString方法中,通过super.toString()来调用Point中的toString(),这一点和我们在Java中的用法一样,不再赘述。
**小结:**本文中,我们学习了用大括号法创建对象的许多简便写法。更重要的是学习了用函数创建对象和用class创建对象的方法,并从内在原理上分析了二者的统一性,从本质上认识到了class语法只是对JavaScript的对象原型继承的一层包装而已。在JavaScript中是没有什么类的概念的。