上一篇随笔提到JS中有两种数据类型:原始类型和对象类型,但是我们还没有提到函数。实际上函数也是一种对象,准确地说函数应该叫做函数对象。下面我们从对象开始说起。
1. 对象
最简单的对象,是这样的:
var obj = {};
我们创建了一个空对象。说它空是因为它没有任何自定义属性,但是实际上它还是有一些默认属性的,这些属性是从它的原型对象继承来的,比如constructor、toString和valueOf等。除此之外,ECMA-262中还定义了对象的一些内置属性,这些属性对JS语言来说是不可见的,也就是说跟我们的编程无关。但是简单地了解一下这些内置属性,对我们深入理解对象的内部机制是有益的。这些属性包括:[[Prototype]]、[[Extensible]]、[[Get]]、[[Put]]、[[HasProperty]]和[[Delete]]等,具体的含义在此不提,只需要知道,只要是对象就一定有这些属性。
2. 函数
接下来我们说一下函数。我们经常听人说“函数是JS中的一等公民,函数可以做参数,可以做返回值”。其实原因很简单,函数也是一种对象,是一种特殊的对象。既然是对象,当然是一等公民了。它只是比普通的对象多了一些特殊的性质,所以看起来跟普通对象有所不同。那么,函数与普通对象比有哪些不同呢?
(1) 从语言规范或者说从JS引擎的角度来看,函数除了前面提到的那些内置属性外,还多了一些内置属性,包括:
- [[Code]]:函数的代码,即函数体里面的那些语句。
- [[FormalParameters]]:函数的形参列表。
- [[Scope]]:词法环境(Lexical Environment),它确定了函数执行时所处的环境。这个东西很重要,跟我们经常听说的作用域链和闭包有关系。
- [[Call]]:执行代码,即在调用此函数时执行的动作。
- [[Construct]]:构造一个新的对象的动作。拥有这个内置属性的函数被称为构造函数,通过new操作符可以用它来产生新的对象。
- [[HasInstance]]:用来检测一个对象是否是此函数构造的。
为了强调函数是一种对象,我们甚至可以把函数想象成对象的形式:
// 某个求较大值的函数的对象表示形式 var max = { _callable_: true, _args_: ['a', 'b'], _code_: 'return a > b ? a : b;' };
(2) 由于函数是一种特殊的以及非常重要的对象,所以JS语言专门提供了特殊的语法来创建函数,即
// 函数声明 function sayHello() { console.log('Hello'); } // 函数表达式 var sayHello = function() { console.log('Hello'); }
当然,也可以用类似于创建普通对象的方式(即new+构造函数)来创建函数,即
var sayHello = new Function("console.log('Hello');");
是不是跟普通对象的方式很像啊:
var data = new Date('2015/09/10');
显然Function是一个非常特殊的构造函数,因为一般的构造函数只能创建普通对象,但是Function能创建一个函数对象。
(3) 函数是可调用的。我们通过函数调用表达式来调用一个函数,从而完成实现某种特定的功能,例如:
function add(x, y) { return x + y; } var z = add(3, 4); console.log(z); // 输出7
这也是我们使用函数最常见的目的。
3. 构造函数
其实上面已经顺带把构造函数也讲了。构造函数是指可以利用 new 操作符来创建对象的函数,这个过程是通过内置属性[[Construct]]实现的。比如Object就是一个构造函数:
var obj = new Object();
实际上,我们在程序里使用函数声明、函数表达式或Function创建出来的函数都是即使普通函数也是构造函数。它们既可以被调用以得到返回值或产生某种效果,也可以用new操作符来创建对象。但是从本质上讲构造函数比普通函数要更特殊一些。
另外为了进行区分,在命名时,前者通常首字母小写,后者通常首字母大写。举一个构造函数的例子:
function Person(name, age) { this.name = name; this.age = age; }
可能有人要问了,是否存在不是构造函数的函数?当然是存在的,比如JS中的那些内置函数基本都属于这种情况。例如:
typeof parseInt; // 结果为"function" parseInt('123'); // 结果为123 new parseInt(); // 抛出TypeError异常,提示function parseInt() { [native code] } is not a constructor
可见parseInt可以作为一个函数来调用,但是不能作为构造函数来创建新的对象。