本文只是关于JavaScript函数的一些入门介绍,写的有些宽泛和简单,目的是对JavaScript的函数有一个大概的理解。
函数定义
声明函数
一般创建(声明)一般分成四部分:
- function关键字
- 函数名称
- 函数的参数列表
- 函数体:包含需要执行的所有语句。特别注意return语句,它决定了调用函数的返回值。
一般格式如下示例:
function findMax(a, b) {
if (a >b ) {
return a;
} else {
return b;
}
}
//函数调用
var maxNumber=findMax(12, 56);
函数表达式
把上面函数声明中的代码去除函数名称,就得到了一个函数表达式。一般可以把一个函数表达式赋值给一个变量,
然后我们就可以把这个变量当成函数名称一样使用,进行函数调用。把上面的函数声明修改成表达式方式的创建,如下示例
var findMax = function (a, b){
if (a >b ) {
return a;
} else {
return b;
}
}
//函数调用
var maxNumber=findMax(12, 56);
使用 new Function()创建函数
除了上面的函数声明和函数表达式的方式创建函数,我们也可以使用new Function()
的方式创建函数。new Function()
传入的最后一个参数是函数体,
其他参数是创建的函数的函数参数。上面的示例修改成new Function()
方式如下:
var fundMax=new Function('a','b','if(a>b){return a;} else{return b;}');
//函数调用
var maxNumber=findMax(12, 56);
自调用函数
自调用函数一般用于一次性调用,而相比于直接把函数体中的代码写在全局作用域,这种方式可以避免全局作用域变量污染。很多JS类型就是使用这种方式编写的。
示例代码如下:
var maxNumber = (
function(a, b){
if (a >b ) {
return a;
} else {
return b;
}
}
)(12,56);
函数声明的提升(Hoisting)
函数声明的提升与变量声明的提升是一样的概念,JS解析器会把函数的声明提升到作用域的最上面执行。所以可以在声明函数之前进行函数调用。
注意:函数表达式和new Function()的方式创建函数是不会被提升的。
如下示例:
//在函数声明前进行函数调用
var maxNumber=findMax(12, 56);
//函数声明
function findMax(a, b) {
if (a >b ) {
return a;
} else {
return b;
}
}
函数是一种特殊的对象
JS中除了值类型(string/number/boolean/null/undefined)的变量,其他变量都是引用类型。而所有的引用类型的变量都是继承Object对象。
函数也是一种特殊的对象,也是继承自Object。可以使用如下代码进行测试。
function findMax(a, b){
if (a >b ) {
return a;
} else {
return b;
}
}
//所有的函数都是Function类型的
console.log(findMax instanceof Function); //输出true
//而Function是继承自Object,所以所有的函数也是继承自Object。(也就是所有的函数都是Object的子类)
console.log(findMax instanceof Object); //输出true
函数参数
函数的显示参数和隐式参数
在函数声明中的参数列表的参数都称为显示参数,也叫做形参。
在函数调用时传入的所有的参数都称为隐式参数,也叫做实参。
如下代码示例:
//这是函数声明。a和b两个参数称之为函数的显示参数,也叫做形参。
function findMax(a, b) {
if (a >b ) {
return a;
} else {
return b;
}
}
//这是函数调用。传入的12和56成为函数的隐式参数,也叫做实参。
var maxNumber=findMax(12, 56);
函数参数的一些规则
JS是弱类型语言,所以函数参数存在如下一些规则:
- 声明函数的时候不需要指定形参的数据类型
- 函数调用的时候不会对实参的数据类型和个数进行检测。
如果需要对函数参数的数据类型和个数进行检测,则需要自己在函数内部编写代码进行参数类型和个数的检测。
函数参数的默认值
很多语言都可以对函数的参数设置默认值:当调用函数时,对应的参数没有传入,则该参数的值就为设置的默认值。
但是在ES6版本
之前的JS语法是不支持函数参数默认值设置的。所以我们使用如下的方式实现函数参数默认值的效果:
function findMax(a, b) {
//相当于给参数a设置默认值为0
a = a || 0;
//相当于给参数b设置默认值为0
b = b || 0;
if (a >b ) {
return a;
} else {
return b;
}
}
函数的arguments对象
在函数体中我们可以使用arguments对象来获取函数调用时传入的实参数据。这并不受形参的影响。如下代码示例:
//没有一个形参
function findMax(){
//使用使用arguments.length获取实参个数,可以像数组一样访问实参的值
for(var i=0, maxNumber=arguments[0]; i< arguments.length; i++){
if(arguments[i] > maxNumber){
maxNumber = arguments[i];
}
}
return maxNumber;
}
//函数调用传入的实参
var maxNumber=findMax(12,-1,34,544,55,0);
函数参数的值传递和引用传递
数据类型分成两大类:值类型和引用类型。如果函数调用时,实参是值类型就成为值传递,实参为引用类型就成为引用传递。如果需要深入了解其中的原理,
就需要知道值类型和引用类型变量的内存分配机制和内存中堆
和栈
的知识。这里不做深入,相关知识网上有很多。
这里通过如下示例解释一下参数的值传递和引用传递。
function double(number,numberList){
number *= 2;
for(var i=0; i< numberList.length; i++){
numberList[i] *= 2;
}
}
var num=3,
numList=[3, 5];
double(num,numList);
//因为num是值类型,所以num在double(num,numList)函数执行后值没有改变:3
console.log(num);
//因为numList是引用类型,所以numList在double(num,numList)函数执行之后值发生了改变:[6,10]
console.log(numList);
函数调用
把函数调用表达式看作一个值使用
可以把函数调用看作一个值来使用,这个值是由函数的返回值决定的。如下示例代码:
function findMax(){
for(var i=0,maxNumber=arguments[0]; i<arguments.length; i++){
if(maxNumber<arguments[i]){
maxNumber=arguments[i];
}
}
return arguments[i];
}
//这里我们就可以把函数调用findMax(12,45,2)看作一个整数,因为该函数调用表达式返回一个整数。
var maxDouble=findMax(12,45,2) * 2;
全局函数
在全局作用域中创建的函数叫做全局函数。而web应用中所有的全局变量和函数都属于全局对象window
的属性。
所以我们可以把全局函数看作是window对象的方法。如下示例代码:
function sum(a, b){
return a+b;
}
var a;
//下面两个调用是等价的
a= sum(1,3);
a=window.sum(1,3);
函数中的this关键字
函数中的this是一个关键字,this指向调用当前函数的对象。如下示例:
function setFirstName(firstName){
this.firstName=firstName;
}
//在全局作用域中直接调用,则this指向全局对象(window)。因为在全局调用setFirstName()相当于window.setFirstName()
setFirstName("james"); //等价于window.setFirstName("james");
var obj={
firstName:"pual",
setFirstName:setFirstName
};
//这是this指向obj对象。
obj.setFirstName("james");
//把函数当成构造函数使用时,this指向构造的对象。
var obj1=new setFirstName("james");
函数作为方法调用
当函数作为对象成员的时候,我们称该函数为对象的方法。这时我们可以通过对象来调用该方法。
如下示例:
var person={
firstName:"james",
lastName:"young",
getFullName:function(){
return this.firstName + " " + this.lastName;
}
};
//通过对象调用该对象的方法
var fullName=person.getFullName();
构造函数
构造函数本身就是一个普通的函数。所以所有的函数都可以是构造函数,只需要在函数调用的前面加上new
关键字时,
我们就称该函数为构造函数。但是构造函数的一般情况下主要目的是用于构造对象。
如下示例:
function Person(name,age){
this.name=name;
this.age=age;
this.grow=function(){
this.age += 1;
}
}
//使用上面的函数构造对象,则这个函数就称为构造函数了
var person=new Person("赵四",34);
//使用对象成员(属性和方法)
console.log(person.name);
person.grow();
函数的call和apply方法
函数是对象类型的,所以函数也有属性和方法。函数有两个常用的方式:call
和apply
。
如果存在一个函数dumpObject
,即便它不是某个对象obj
的方法,
也可以通过dumpObject
函数的call
或者apply
方法实现:对象obj
对函数dumpObject
的调用,
也就是函数dumpObject
中的this
关键字指向对象obj
。
如下示例:
function dumpObject(prefix,suffix){
var dumpString=prefix;
for(var prop in this){
dumpString += prop + ':' + this[prop] + '\n\r';
}
dumpString += suffix;
console.log(dumpString);
}
//创建一个对象,该对象不存在dumpObject方法
var obj={
name:"赵四",
age:78,
score:70
};
//可以使用函数的call或者apply方法实现:obj像调用自己方法一样的效果调用dumpObject函数
var prefix='***start***\n\r',
suffix='***start***\n\r';
dumpObject.call(obj,prefix,suffix);
//从函数调用中可以看出,apply与call的唯一不用是:apply方法通过数组的形式,把函数dumpObject的参数组织成apply的一个参数。
dumpObject.apply(obj,[prefix,suffix]);
闭包
什么是闭包
闭包的概念其实很简单,一句话就可以表达。
闭包是可以访问上一层函数作用域里变量的函数,即便上一层函数已经关闭。
但是需要理解这句话还是需要些功夫的。首先提取一下这句话,得到三个关键点:
- 闭包是函数
- 闭包可以访问上一层函数作用域变量:闭包是内嵌函数,而且可以访问了上一层函数的局部变量。
- 即便是上一层函数已经关闭:需要把闭包使用
return
语句返回。
还需要清楚的一个概念是:JavaScript中所有函数都能访问它们上层作用域中的变量。
闭包实现一个计数器的实例代码:
//使用一个自调用函数,得到了一个闭包
var counter=(
function(){
var count = 0;
//返回的函数就是闭包,访问了上一层函数的count局部变量
return function(){
count += 1;
}
}
)();
为什么需要闭包
上面介绍了什么是闭包。但是我们为什么需要闭包呢?需要明白这个问题前需要了解局部变量
和全局变量
的局限性,已经闭包的特性。
局部变量
的特点是:函数执行完就被销毁,所以无法实现变量的累积(如计数器的功能)。
全局变量
的特点是:在全局都可以访问,只有在页面关闭后才销毁。可以实现指定的计数器功能。问题在于:全局变量是所有地方都可以修改,
而且尽量少使用全局变量,避免全局变量污染
。
针对全局变量
和局部变量
在这种场景下存在的问题,闭包
的相关特性正好能够解决这个问题。因为闭包可以使用上一层函数的局部变量,
这样避免了全局变量的污染
。而且因为闭包被上一层函数返回,并且被外部引用,所以闭包引用了的上层函数的变量在上层函数结束后依然不会被销毁。
这样又避免了普通局部变量
在函数执行完就被销毁,无法累积的问题。
所以这就是闭包的特性。也是闭包被使用的场景。
在编写javascript代码时,我们也要警惕闭包
带来的问题:引用的局部变量迟迟不能释放,容易导致内存的泄露。
这里只是简单的介绍了一下闭包,以后有机会可以专门写一篇关于闭包
的文章。