1、函数也是对象
一般情况下,Java程序员可能习惯这样的函数代码格式:
但是,由于JavaScript的函数本质上也是一个对象,上述定义的printNum函数实际上是一个函数对象,而函数名printNum可以视为指向该函数的变量的名字。
因此,第二种定义函数的方式如下:
注意这种方式末尾大括号后面要加个分号。
以上两种写法定义完函数后,都需要单独的一行代码进行调用: printNum(333);
其实还有一种写法,定义函数和调用函数写在一起:
根据实际情况,使用不同的写法即可。
2、函数参数个数
上面的函数有一个参数 num ,但是调用函数的时候,传几个参数,甚至不传参数,其实都不影响调用,因此传入的参数比定义的参数多也没有问题,虽然函数内部并不需要这些参数:
如果要避免收到参数传递错误,影响程序逻辑结果的情况发生,可以对参数进行检查:
注意上面代码中的typeof 关键字这种判断方式,返回的是参数的类型的名字,这个名字本身是个字符串,所以我们判断等于(===
)或者不等于(!==
)一个字符串名字。
另外,除了打印出错误提醒,JS中还可以直接抛出异常:
异常使用throw关键字抛出,关于异常,后面会详细介绍。
JavaScript函数还有一个关键字arguments,它只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数。arguments类似Array但它不是一个Array:
利用arguments,你可以获得调用者传入的所有参数。也就是说,即使函数不定义任何参数,还是可以拿到参数的值:
arguments 最常用于判断传入参数的个数。比如定义的参数有三个,但是如果调用的时候,只传入了两个参数,想算作第一个和第三个,如果想实现这样的方式,可以下面这样写:
这样就把中间的参数变为“可选”参数,实现这个只能通过arguments判断,然后重新调整参数并赋值。
正常传参情况下,为了获取除了已定义参数之外的参数(比如已定义三个),我们不得不用arguments,并且循环要从索引3开始以便排除前三个参数,这种写法很别扭,只是为了获得额外的多传的参数,有没有更好的方法?
ES6标准引入了rest参数,动态的多余的函数可以改写为:
rest参数只能写在最后,前面用 ...
标识,从运行结果可知,传入的参数先绑定x、y、z,多余的参数以数组形式交给变量rest,所以,不再需要arguments我们就获取了全部参数。
如果传入的参数连正常定义的参数都没填满,也不要紧,rest参数会接收一个空数组(注意不是undefined)。
注意rest参数是ES6新标准,所以你需要确认浏览器是否支持。
3、函数 return
语句
JavaScript引擎有一个在行末自动添加分号的机制,所以代码中每一行末尾其实不写分号也行:
但是要注意,末尾不加分号时,函数的return语句可能会有一个大坑,
这里把return语句拆成两行,第一行后面就会直接加分号,不再返回第二行的数据。所以正确的多行写法是加大括号:
Java程序员要特别注意,因为在Java里面是可以多行的(下图是Java代码):
所以无论什么代码,保持良好的代码风格很重要。
4、变量作用域
在JavaScript中,用var申明的变量实际上是有作用域的。如果一个变量在函数体内部申明,则该变量的作用域为整个函数体,在函数体外不可引用该变量:
如果两个不同的函数各自申明了同一个变量,那么该变量只在各自的函数体内起作用。换句话说,不同函数内部的同名变量互相独立,互不影响:
JavaScript的函数是可以嵌套的,此时,内部函数可以访问外部函数定义的变量,反过来则不行:
这说明JavaScript的函数在查找变量时从自身函数定义开始,从“内”向“外”查找。因此,函数里面更内部的函数定义的变量是访问不到的。
如果更内部函数定义了与外部函数重名的变量,则内部函数的变量将“屏蔽”外部函数的变量。但是仅仅在内部函数内有效。
变量提升:意思是JavaScript的函数定义有个特点,它会先扫描整个函数体的语句,把所有申明的变量定义“提升”到函数顶部,但是变量的赋值并没有提升:
虽然是strict模式,但语句var x = 'Hello, ’ + y;并不报错,原因是变量y在稍后申明了。但是console.log显示Hello, undefined,说明变量y的值为undefined。这正是因为JavaScript引擎自动提升了变量y的声明,但不会提升变量y的赋值。
对于上述foo()函数,JavaScript引擎看到的代码相当于:
由于JavaScript的这一怪异的“特性”,我们在函数内部定义变量时,请严格遵守“在函数内部首先申明所有变量”这一规则。如果是Java程序员的话,遵循Java开发的习惯其实非常好,能有效避免JavaScript的很多坑。
5、全局对象 window
不在任何函数内定义的变量就具有全局作用域,例如:
实际上,JavaScript默认有一个全局对象 window
,全局作用域的变量实际上被绑定到 window
的一个属性,由于 window
是客户端浏览器对象模型的基类,window
对象是客户端 JavaScript 的全局对象,所以访问window全局对象,需要在浏览器中才可以,直接在JS代码中运行就会报错:
改成在浏览器中运行的函数 alert
即可:
在浏览器中运行,效果如下:
因此,直接访问全局变量 a
和访问 window.a
是完全一样的。
由全局变量可以联想到全局函数,由于函数定义有两种方式,以变量方式 var foo = function () {}
定义的函数实际上也是一个全局变量,因此,顶层函数的定义也被视为一个全局变量,并绑定到window对象:
浏览器中运行效果如下:
进一步大胆地猜测,我们每次直接调用的alert()函数其实也是window的一个变量。这说明JavaScript实际上只有一个全局作用域。任何变量(函数也视为变量),如果没有在当前函数作用域中找到,就会继续往上查找,最后如果在全局作用域中也没有找到,则报ReferenceError错误。
6、自定义名字空间
全局变量会绑定到 window
上,不同的JavaScript文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。
减少冲突的一个方法是把自己定义的所有变量和函数全部绑定到一个自己定义的全局变量下变量中(其实就是一个自定义对象)。例如:
把自己的代码全部放入唯一的名字空间 myParam
中,会大大减少全局变量冲突的可能。而且,许多著名的JavaScript库都是这么干的:jQuery,YUI,underscore等等。
7、局部作用域
由于用 var
定义的 JavaScript的变量作用域实际上是函数内部,我们在for循环等语句块中是无法定义具有局部作用域的变量的:
为了解决块级作用域,ES6引入了新的关键字let,用let替代var可以申明一个块级作用域的变量:
上面代码中 循环参数 i
只能在for循环内部访问。
8、常量
由于var和let申明的是变量,如果要申明一个常量,在ES6之前语法上是不行的,我们通常用假装申明了一个常量,全部大写的变量来表示“这是一个常量,不要修改它的值”:
var HELLO_MSG = "Hello World";
ES6标准引入了新的关键字const来定义常量,const与let都具有块级作用域:
像字符串,数字等等这些都是非常常用的基本的数值,如果是一个引用对象,则可以改变,比如数组:
9、解构赋值
从ES6开始,JavaScript引入了解构赋值,可以同时对一组变量进行赋值。什么是解构赋值?我们先看看传统的做法,如何把一个数组的元素分别赋值给几个变量:
var array = ['hello', 'JavaScript', 'ES6'];
var x = array[0];
var y = array[1];
var z = array[2];
现在,在ES6中,可以使用解构赋值,直接对多个变量同时赋值:
注意,对数组元素进行解构赋值时,多个变量要用 [...]
括起来。
如果数组本身还有嵌套(也就是多维数组),也可以通过下面的形式进行解构赋值,注意嵌套层次和位置要保持一致:
解构赋值还可以忽略某些元素:
如果需要从一个对象中取出若干属性,也可以使用解构赋值,便于快速获取对象的指定属性:
对一个对象进行解构赋值时,同样可以直接对嵌套的对象属性进行赋值,只要保证对应的层次是一致的:
使用解构赋值对对象属性进行赋值时,如果对应的属性不存在,变量将被赋值为undefined,这和引用一个不存在的属性获得undefined是一致的。如果要使用的变量名和属性名不一致,可以用下面的语法获取:
解构赋值还可以使用默认值,这样就避免了不存在的属性返回undefined的问题:
有些时候,如果变量已经被声明了,再次赋值的时候,正确的写法也会报语法错误:
解构赋值在很多时候可以大大简化代码。例如,交换两个变量x和y的值,可以这么写,不再需要临时变量:
var x=1, y=2;
[x, y] = [y, x]
全局对象 window
下有一个属性对象 location
,而 location
对象下的属性可以用于快速获取当前页面的域名和路径:
domain 表示域名,path表示路径,运行效果如下:
window.location 对象可不带 window 前缀书写(如上面的代码),一些主要的属性例子如下:
window.location.href 返回当前页面的 href (URL)
window.location.hostname 返回 web 主机的域名
window.location.pathname 返回当前页面的路径或文件名
window.location.protocol 返回使用的 web 协议(http: 或 https:)
window.location.assign 加载新文档
如果一个函数接收一个对象作为参数,那么,可以使用解构直接把对象的属性绑定到变量中。例如,下面的函数可以快速创建一个Date对象:
function buildDate({year, month, day, hour=0, minute=0, second=0}) {
return new Date(year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second);
}
它的方便之处在于传入的对象只需要year、month和day这三个属性:
也可以传入hour、minute和second属性:
使用解构赋值可以减少代码量,但是,需要在支持ES6解构赋值特性的现代浏览器中才能正常运行。目前支持解构赋值的浏览器包括Chrome,Firefox,Edge等。
10、对象中的方法
在一个对象中绑定函数,称为这个对象的方法。在JavaScript中,对象的定义是这样的:
var xiaoming = {
name: '小明',
birth: 1990
};
但是,如果我们给对象 xiaoming
绑定一个函数,就可以做更多的事情。比如,写个age()方法,计算并返回xiaoming的年龄:
绑定到对象上的函数称为方法,和普通函数也没啥区别,但是它在内部使用了一个this
关键字,这个关键字是什么?
在一个方法内部,this
是一个特殊变量,它始终指向当前对象,也就是 xiaoming
这个变量。所以,this.birth
可以拿到xiaoming
的birth
属性。让我们拆开写:
单独调用函数getAge()
怎么会报错?请注意,我们已经进入到了JavaScript的一个大坑里。
JavaScript的函数内部如果调用了this
,那么这个this
到底指向谁?答案是,视情况而定!
如果以对象的方法形式调用,比如xiaoming.age()
,该函数的this指向被调用的对象,也就是xiaoming
,这是符合我们预期的。如果单独调用函数,比如getAge()
,此时,该函数的this
指向全局对象,也就是window
。因为window
中没有birth
这个属性,所以,报错了!
更坑爹的是,对象中的方法还不能分两步调用,如果这么写也会有问题:
因此,要保证this
指向正确,必须用 obj.xxx()
的形式调用!
由于这是一个巨大的设计错误,要想纠正可没那么简单。ECMA决定,在strict模式下让函数的this指向undefined,因此,在strict模式下,你会得到一个错误,就是上面报的错误,普通模式会得到NaN
。
所以,平时我们在调用对象方法的时候,一定要保持良好的习惯,这样能避免很多错误的发生,尤其尤其是在JavaScript这样的语言中!
11、修正this
虽然在一个独立的函数调用中,根据是否是strict模式,this指向undefined或window,不过,我们还是可以手动控制this的指向的!
要指定函数的this指向哪个对象,可以用函数本身的apply方法,它接收两个参数,第一个参数就是需要绑定的this变量,第二个参数是Array,表示函数本身的参数。下面用apply修复getAge()调用:
另一个与apply()类似的方法是call(),唯一区别是:
apply()把参数打包成Array再传入,call()把参数按顺序传入,上面的getAge没有参数,所以后面一个也不用传:
对普通函数调用,我们通常把this绑定为null,比如调用Math.max(3, 5, 4),分别用apply()和call()实现如下:
12、装饰器
利用apply(),我们还可以动态改变函数的行为!
JavaScript的所有对象都是动态的,即使内置的函数,我们也可以重新指向新的函数。
现在假定我们想统计一下代码一共调用了多少次parseInt(),可以把所有的调用都找出来,然后手动加上count += 1,不过这样做太傻了。最佳方案是用我们自己的函数替换掉默认的parseInt():
因为使用了window对象,所以打开浏览器,在浏览器控制台查看结果:
13、总结
虽然JavaScript有很多设计问题,但是只要保持良好严谨的代码设计和调用习惯,很多问题其实都不会碰到!