在 3.函数表达式和this 中,我们其实已经简单探讨过函数的本质,我们知道,函数本质上就是一个对象,那我们其实又知道,所有的对象都是通过构造函数 new 出来的,所以,其实函数,也是通过 new 关键字创造出来的对象,实际上函数是通过 new Function() 创造出来的。
这个构造函数可以传递很多参数,其中,最后一个参数是函数体,前面的均为所创造函数的参数。
js
var fun = new Function("a","b","return a+b");
console.log(fun(1,2));//输出3
console.log(typeof fun);//输出function
/*
相当于创造了函数:
fun(a,b){
return a+b;
}
*/从这里我们更能理解,JS 中,函数本质上就是对象,并且所有的函数都是通过 new Function 创建的。但这里又有一个很神奇的问题,就是这个 Function,作为一个构造函数,他也应该是函数,按照我们的说法,Function 作为一个函数,也应该是由 Function 构造函数生成的,这就套娃了,实际上,Function 函数很特殊,它是 JS 引擎自己实现的一个函数,并不依赖于任何构造函数生成。所以现在我们可以得出一个很准确的结论,除了 Function 函数本身以外,其他所有的函数都是通过 Function 构造函数生成的。
所以其实我们是这么一个流程,首先,JS 先自己创造了一个 Function 函数,然后通过 Function 函数创造了很多构造函数对象,通过这些构造函数又可以创造很多对象。
由于函数本身是一个对象,所以函数中也可以有很多属性和方法。
我们举个例子,比如 Number 这个东西,我们可以使用 typeof Number 来验证,这个东西是一个函数。但是我们发现 Number.isNaN() 这是一个方法,用于判断一个数是否是 NaN,实际上,我们前面使用的 isNaN 方法都是 w indow.isNaN 方法,即挂到 window 对象上的,这两个 isNaN 方法的功能其实是一样的,但是具体实现上有一些细微区别,读者不必太过在意,只需要理解函数本质上是一个对象,这个对象上也可以挂很多属性和方法。
接着这个 Number 函数,我们可以引入包装类的概念首先,Number 这个函数可以创建一个数字,或者说,可以将任意数据类型转换为数字,例如 Number("123") 返回数字 123,Number(true) 返回数字 1,当然, Number(333) 返回的肯定是 333,如果什么参数不传,Number 默认返回 0。但是我们注意到,Number 这个东西首字母是大写的,所以我们能猜到这玩意儿应该是一个构造函数,使用 new 关键字才是它的正确打开方式。
js
var num1 = Number(1);
var num2 = new Number(2);
console.log(num1,num2);
这里就出现问题了,直接使用 Number 函数会返回一个原始类型的数字,这个没问题,但是使用 new 关键字,返回的居然是一个对象,或者说是一个对象类型的数字 2,前面我们提到,数字应该是一个原始类型,这里怎么会存在对象类型的数字呢?这个返回的对象,虽然确实是数字 2,但并不是我们前面提到的数字类型,这个东西叫做包装类。
我们知道,和对象相比,原始类型是非常弱鸡的,他们不能拥有属性和方法,JS 为了增强原始类型的功能,为布尔,字符串,数字,分别创建了一个构造函数,布尔类型的构造函数是 Boolean,字符串的构造函数为 String,数字的构造函数就是前面的 Number。但这并不意味着原始类型就是对象了,就比如 Number(1) 返回的就是原始类型的 1,但是 new Number(1) 返回的就是对象 1,看懂了吧?就是说现在我们说到数字 1,就有原始类型的 1 和对象形式的 1 两种了。并且对象形式的 1 有很多属性和方法。
如果我们把原始类型的数据当做对象来使用,比如访问它的属性和方法,那么 JS 会自动创造一个对象形式的他,从而访问到他的属性和方法。而对象形式的那些原始类型如果像原始类型那样参与运算,也会自动转换为原始类型。这一点非常智能。
举个例子
js
var a = 3.1415926;//原始类型的3.1415926
var b =a.toFixed(2);//访问这个原始类型的一个方法
console.log(typeof a);//number
console.log(b);//"3.14"可以看到,这里的 a 变量其实是一个原始类型,这个原始类型不应该有属性和方法,但是当我把他当做对象去访问这个方法的时候没有报错,这就是由于在访问它的属性和方法的时候,JS 会利用构造函数自动创造一个等价的对象来访问它的方法,相当于 (new Number(a)).toFixed(2)。但这个创造是临时创造,并没有改变 a,所以在输出 a 的时候输出的并不是对象,而是原始类型的 number。
所以以后我们可以把数字,字符串和布尔直接当做对象来用,它里面有一些内置的方法和属性,就像数组的 length 属性一样,这些方法和属性有时候非常好用,但请记住原始类型并不能拥有属性,只不过对于这三种原始类型,JS 为了扩充功能给他加了个 BUFF。例如一个字符串,我要访问它的长度,就可以直接把它当做对象,使用它的 length 属性,比如 console.log("123".length),可以看到,虽然字符串是原始类型,但也可以拥有 length 属性,当访问 length 属性的时候,JS 自动执行 new String("123") ,然后就变成等价的对象形式了,从而可以拥有 length 属性。
类是一个其他语言的概念,在 JS 里面,可以认为类就是构造函数。包装类其实就是用来包装的,用来把一个原始类型包装成一个对象,这就是包装类。包装类的存在允许我们想对象的方式去使用一些原始类型的属性。
这里最后介绍几个概念,所有对象都是由构造函数构造出来的,例如 [1,2,3] 就是由 Array 构造函数构造出来的对象,我们也知道这个数组对象上可以放很多的属性和方法,而 Array 构造函数本身就是一个函数对象,它身上也可以拥有很多属性和方法。也就是说对于这个 Array 构造函数来说,有一些属性和方法是自带的,可以直接通过 Array 这个函数对象去访问,但是有一些属性和方法是必须把它作为构造函数,构造出来一个数组对象,这些属性和方法只能通过由它创造的数组对象才能访问到,例如 length 属性,我们无法通过 Array 这个构造函数访问 length 属性,但是如果我们通过 Array 构造出一个数组对象,这个数组对象就可以访问 length 属性。
注:这是我随便举的例子,并不严谨,实际上,任何一个函数都有一个 length 属性,但这个 length 属性和数组的 length 属性并不一样,数组的 length 属性用于返回这个数组对象最大下标+1,但是函数的 length 属性用于返回这个函数期望的参数数量,这两个方法虽然同名,但完全不一样。
更合适的例子是,Number 构造函数有 isNaN 方法,但通过 Number 创造出的对象,却没有该方法。
这里对于包装类只是简要介绍,如果要详细了解参照3.包装类
对于一个构造函数而言,由这个构造函数构造出来的对象我们称为这个构造函数的实例,有时候也称作成员。而对于属性(方法)而言,如果通过这个构造函数可以直接访问到的属性(方法),我们称为静态属性(方法),如果是这个构造函数自己访问不到,只有这个构造函数的实例才能访问到的属性(方法),我们称为实例属性(方法)。
我们总结一下加深理解,首先,任何一个对象都是通过构造函数生成的,通过 new 关键字可以使用构造函数生成一个对象,这个生成的过程可以称为实例化。我们站在构造函数的视角,我自己就是一个对象,所以自己就可以有很多属性和方法,直接通过自己的函数名就可以调用,这种可以通过自己的函数名调用的属性和方法叫做这个构造函数的静态属性(方法),或者也可以成为类属性(方法)。而有一些属性(方法),是必须使用 new 关键字,利用自己生成一个对象,然后这些属性(方法)是绑定在由自己生成的对象上的,这种属性或方法我们称为成员属性(方法)或者也称为实例属性(方法)。
举个例子
js
function Test(){
this.a = "我是实例属性";
this.fn1 = function(){
console.log("我是实例方法");
}
}
Test.fn2 = function(){
console.log("我是静态方法");
}
Test.b = "我是静态属性"
var obj = new Test();//obj为Test构造函数创造出来的实例
obj.fn1();//实例方法通过构造函数创造的实例访问
Test.fn2();//静态方法通过构造函数本身来访问