函数
函数包含一组语句,是JS的基本模块单元,用于代码复用、信息隐藏、组合调用。函数用于指定对象的行为。一般来说,所谓编程就是讲一组需求分解成一组函数与数据结构的技能。
函数对象
在JS中函数就是对象,是一个特殊的对象,是Function
类的实例。
function fn(){
}
// 函数就是对象,只是一个特殊的对象,函数Function类的实例。
console.log(typeof fn);// function
函数对象是“名值对”的集合并拥有一个连接原型对象的隐藏连接。
函数对象在内存中使用“键值对”存储,函数名作为键名(栈),函数体作为值并指向内存中一个名为Function的对象。
每个函数在创建时拥有两个附加的隐藏属性:
- 函数的上下文
- 实现函数行为的代码
每个函数在创建时拥有一个prototype
属性,其值是一个拥有constructor
属性且值为该函数的对象。
这和隐藏连接到Function.prototype
完全不同。
ES中的函数对象主要有3种作用:
- 作为
object
对象使用,函数对象本身是一种增强的object
对象,这是主要使用其中的属性。 - 作为方法来处理具体业务,这是函数最常见的用法。
- 用户创建
object
类型对象
对应函数的3种作用,函数对象有3种子类型:
- 自身属性
- 内部变量
- 所创建对象的属性
函数定义/创建函数
ES中创建函数有3种方式:
- 函数声明
- 函数表达式
- 使用
Function
类来创建
常见定义函数有两种方式:
- 函数声明
// 函数声明
function 函数名(参数){
函数体
}
function log(msg){
console.log(msg);
}
关于函数声明,有一个重要特征是函数声明提升,在执行代码前会先读取函数声明,意味着可以把函数声明放在调用它的语句之后。
log('hello');// 代码执行前会先读取函数声明
function log(msg){
console.log(msg);
}
- 函数字面量/函数表达式
函数表达式的结构是将函数声明中的函数名去掉,将创建后的结果赋值给一个变量。
// 由于函数是一个对象,所以可使用变量形式进行定义,看起来好像是常规的变量赋值语句。
// 这种情况下创建的函数叫做匿名函数/拉姆达函数(anonymous function),因为function关键字后没有标识符。
// 函数表达式与其他表达式一样,在使用前必须先赋值。
log('hello');// Uncaught TypeError: log is not a function
var log = function(msg){
console.log(msg);
}
使用对象字面量产生的对象连接到Object.prototype
,函数对象连接到Function.prototype
,该原型对象本身连接到Object.prototype
。
- 使用
Function
类创建
Function
类是ES中一个内置的特殊的function
。
console.log(typeof Function); // function
Function
是一个特殊的function
,使用它创建的对象并不是object
类型,而是function
类型。
var log = new Function(‘msg’, 'console.log(msg);');
console.log(typeof log);// function
使用Function
类创建函数时,编译器不会检查其正确性,如果有错误只有在调用时才能发现,在创建时不会报错。另外,使用Function
类创建的函数效率相对而言比较低。
不同创建函数方法之间的关系是什么样的呢?
在ES中所有的数据只有两种存在的形式:
- 对象属性
- 变量
函数也不例外,无论是对象的属性还是变量都是“名值对”的结构,所以函数在内存中也采用这种“名值对”的结构。
使用函数表达式方式创建函数中,可以很容易看明白这一点。函数表达式将所创建的对象赋值给一个变量。其实,在函数声明方式创建函数的时候,ES在背后自动帮我们做了很多事情,首先它创建了函数对象,然后又创建了跟函数名同名的变量,最后将创建出来的函数赋值为变量。
function fn(){
}
// // 首先创建了函数对象,然后又创建了跟函数名同名的变量,最后将创建出来的函数赋值为变量。
var fn = function fn(){
}
function fn(){
}
// 函数是通过对象的拷贝来实现赋值
var fn2 = fn;
为进一步理解
function log(msg){
console.log(msg);
}
var fn = log;
log = null;
fn('hello');// hello
函数表达式
通过函数表达式来创建同样,只是它会创建一个匿名函数,然后在赋值给定的变量。
var anonymous = function(){
}
var anony = anonymous;
函数虽然是一个对象,但与对象却有区别:对象是通过引用的指向来完成对象的赋值,而函数却是通过对象的拷贝来实现的。
- 函数通过对象的拷贝来实现赋值
function fn1(){
alert('fn1');
}
var fn2 = fn1;
fn1 = function(){
alert('fn1 change');
}
fn1();//fn1 change
//函数是通过对象的拷贝来实现赋值
fn2();//fn1
- 对象通过引用指向实现赋值
var obj1 = new Object()
var obj2 = obj1;
console.log(obj1.name);// undefined
obj1.name = 'alice';
console.log(obj1.name);//alice
console.log(obj2.name);//alice
小结:函数的创建主要有三部分:函数名、参数、函数体,其中参数和函数体属于函数对象自身,而函数名是获取函数对象的一个引用,可视为一个函数对象的快捷方式。函数表达式会直接将所创建的函数返回给一个快捷方式,而函数声明会在内部自动创建一个和函数同名的快捷方式。
函数重载
函数的参数和调用没有关系
function sum(num1){
return num1;
}
function sum(num1,num2){
return num1+num2;
}
alert(sum(10));//NAN
alert(sum(10,20));//30
函数是对象,不存在重载仅存在覆盖。
function sum(num1,num2){
return num1+num2;
}
function sum(num1){
return num1;
}
alert(sum(10));//10
alert(sum(10,20));//10
为便于理解,转换为变量写法:
var sum = function(num1,num2){
return num1+num2;
}
//此时sum所指向的内存空间已经从两个参数的函数转变为仅有一个参数的函数中
var sum = function(num1){
return num1;
}
alert(sum(10));//10
alert(sum(10,20));//20
总结:在Javascript中函数不存在重载
函数即对象
ECMAScript最令人感兴趣的可能莫过于函数实际上是功能完整的对象。
var funcname = new Function(arg1,arg2,...,argN,funcbody);
// 函数定义的对象方式
var fn = new Function('num1','num2','alert(num1+num2)');
fn(10,20);//30
//转变为函数定义写法为
function fn(num1,num2){
alert(num1+num2);
}
fn(10,20);//30
函数的传递
由于函数就是对象,所以可以将函数作为参数进行传递。
function callFn(fn,arg){
return fn(arg);
}
function say(str){
alert('hello,'+str);
}
callFn(say,'javascript');//hello,javascript
为便于理解函数即对象,可将say函数转换为参数形式:
var say = function(str){
alert('hello,'+str);
}
为理解函数即对象,可将函数作为返回值。
function fn(arg){
var rel = function(num){
return arg+num;
}
return rel;
}
var func = fn(10);//此时func是一个函数对象,可完成调用。
alert(func(20));//30
//简化写法
fn(10)(20);
例:Javascript排序
Javascript sort()
- 用法:
sort()
用于对数组元素进行排序 - 语法:
arrayObject.sort(sortby)
- 参数:
sortby
可选规定排序顺序,必须是函数。无参数时将按字母顺序对数组元素排序,实质上是按字符编码顺序进行排序。 - 返回值:数组在原数组上进行排序,不生成副本。
var as = [-2,31,50,3,50,-29];
function sortByNum(a,b){
return a-b;
}
as.sort();
console.log(as);//[-2, -29, 3, 31, 50, 50]
若要按照其他标准排序,需提供比较函数,该函数比较两个值,返回一个用于说明两个值相对顺序的数字。
比较函数应具有两个参数a和b,其返回值为:
- a<b 排序后数组中a在b前,则返回小于0的值。
- a=b 返回0
- a>b 返回大于0的值
扩展:按数值排序
function sortByNum(a,b){
return a-b;
}
var numarr = [-2,31,50,3,50,-29];
numarr.sort(sortByNum);
console.log(numarr);//[-29, -2, 3, 31, 50, 50]
当数组中存在字符串类型的数字时,并不影响数值排序,因为在Javascript中 <kbd>+</kbd> 被用作字符串连接运算符,而 <kbd>-</kbd> 仍旧是算术运算符。
var arr = [0,2,'-2',20,'11',1,'190'];
arr.sort(function(a,b){
return a-b;//字符串数字与数值进行减法运算时会将字符串转换为数值后计算
});
console.log(arr);//["-2", 0, 1, 2, "11", 20, "190"]
若出现数字和字符串混合形式的数组元素,该怎么办呢?
var arr = [0,'2',-1,'3px',20,13,9];
arr.sort(function(a,b){
return parseInt(a) - parseInt(b);//手工转换为整数
});
console.log(arr);//["-2", 0, 1, 2, "11", 20, "190"]
扩展: 根据对象排序
根据对象属性进行排序
function Person(name,age){
this.name = name;
this.age = age;
}
var p1 = new Person('carl',29);
var p1 = new Person('vivi',18);
var p1 = new Person('mark',30);
var arr = [p1,p2,p3];
//按姓名排序
function sortByName(obj1,obj2){
if(obj1.name > obj2.name){
return 1;
}else if(obj1.name == obj2.name){
return 0;
}else{
return -1;
}
}
//按年龄排序
function sortByAge(obj1,obj2){
return parseInt(obj1.age) - parseInt(obj2.age);
}
//数组排序
arr.sort(sortByName);
//页面指定位置输出
function show(id,arr){
var el = document.getElementById(id);
for(var i=0; i<arr.length; i++){
el.innerHTML += arr[i].name + ' '+arr[i].age+'<br/>';
}
}
show('person',arr);
由于对象属性不固定,按对象属性排序非常不灵活,解决此问题可采用使用函数返回值调用。
function Person(name,age){
this.name = name;
this.age = age;
}
var p1 = new Person('zen', 60);
var p2 = new Person('alice',20);
var p3 = new Person('ben', 28);
var arr = [p1,p2,p3];
//使用函数返回调用按对象属性排序
function sortByAttr(attr){
var fn = function(obj1,obj2){
if(obj1[attr] > obj2[attr]){
return 1;
}else if(obj1[attr] == obj2[attr]){
return 0;
}else{
return -1;
}
};
return fn;
}
//按对象名称排序
arr.sort(sortByAttr('name'));
//按对象年龄排序
arr.sort(sortByAttr('age'));
函数内部属性
arguments
在函数对象中有一个属性叫作arguments,通过此属性可获取相应的参数值,此属性是一个数组,其实就是传递进来的参数。
function fn(){
console.log(arguments);//[1, 2, 3, 4, 5, 6]
for(var i=0; i<arguments.length; i++){
console.log(arguments[i]);
}
}
fn(1,2,3,4,5,6);
递归求阶乘
//使用递归求阶乘的方法
function factorial(num){
return num<=1 ? 1 : num*factorial(num-1);
}
factorial(3);//6
var fn = factorial;
console.log(fn(3));//6
使用递归求阶乘的方式中,递归调用的函数名和函数自身的名称耦合在一起,若将来函数名称更改后,递归就会失效。
function factorial(num){
return num<=1 ? 1 : num*factorial(num-1);
}
var fn = factorial;
factorial = null;
fn(3);// Uncaught TypeError: factorial is not a function
使用argument.callee()来反向调用函数。
function factorial(num){
return num<=1 ? 1 : num*arguments.callee(num-1);
}
console.log(factorial(3));//6
小结:使用arguments对象中的callee(),传入函数参数可反向调用函数以实现函数名的解耦合。在Javascript中通常都是此种方式来完成递归。
this
当需要创建一个类的时候,设置类的属性和方法需通过this关键字来引用。但是需要特别注意的是this关键在在调用时会根据不同的调用对象变的不同。
var color = 'red';
function show(){
console.log(this.color);
}
function Circle(color){
this.color = color;
this.show = show;
}
var circle = new Circle('yellow');
circle.show();//yellow:此时调用的对象为circle
show();//red:此时调用的对象等于window,因此此处的this就是window
函数属性
函数的属性是能够直接使用函数名来调用的属性,函数有2个非常重要的属性length和prototype,length指的是该函数所期望传递过来的参数个数。
length
函数的length表示该函数所期望的参数个数。
function fn1(){}
function fn2(arg1){}
function fn3(arg2){}
console.log(fn1.length);//0
console.log(fn2.length);//1
console.log(fn3.length);//2
函数还具有2个非常有趣的方法call()和apply(),它们可通过函数名称来调用函数。
call & apply
- call()是与经典对象冒充方法最相似的方法,其语法结构是:call(obect,arg1,arg2,...,argN)。call()的第一个参数用作this的对象(上下文对象),其他参数都直接传递给函数自身。
- apply(object, arglist)有两个参数,第一个是调用的上下文,第二个是参数数组,可直接使用arguments。
- call()和apply()常用于Javascript面向对象的继承中。
function sum(num1,num2){
return num1+num2;
}
function sumCall(num1,num2){
return sum.call(this,num1,num2);//call使用的是参数列表,用于确定的参数个数。
}
function sumApply(){
return sum.apply(this,arguments);//apply使用的参数数组,用于不确定参数个数中。
}
console.log(sumCall(1,2));//3
console.log(sumApply(1,2));//3
使用call()和apply()可发现,对象中可不依赖于任何行为。
var color = 'red';
function show(){
console.log(this.color);
}
function Color(color){
this.color = color;
}
var obj = new Color('yellow');
show.call(this);//red
show.call(obj);//yellow
小结:使用call()和apply()之后,对象中可无需定义任何方法。