JS 进阶(2) ECMAScript5 — 函数深入理解:函数的本质、函数定义、函数调用(匿函数自执行、js方法链式调用..)、函数参数(arguments..)、函数返回值

函数介绍

1.什么是函数

函数就是一次封装,四处调用的代码。

函数分为命名函数匿名函数

2.js中的函数传参和其他语言的区别?

js中传的实参个数,和函数期望的参数个数可以不一致。

3.函数调用时发生了什么?

每一次调用函数都会创建一个全新的局部作用域。

4.为什么使用函数呢?

除了代码的复用,我觉得最大的好处就是减少了代码细节的暴露,增强了代码的可读性

举个例子

//按照季节做事
function doSomethings(month){
	if(3<=month && month<=5){	//春天
		//春游
	}else if(6<=month && month<=8){	//夏天
		//避暑
	}else if(9<=month && month<=11){	//秋天
		//秋游
	}else{	//冬天

	}
}

上面的代码可读性不是太好,时间长了后再看需要反应一会。
但是修改后。

function isSpring(month){
	return 3<=month && month<=5;
}
function isSummer(month){
	return 6<=month && month<=8;
}
function isAutumn(month){
	return 9<=month && month<=11;
}

function doSomethings(month){
	if(isSpring(month)){	//春天
		//春游
	}else if(isSummer(month)){	//夏天
		//避暑
	}else if(isAutumn(month)){	//秋天
		//秋游
	}else{	//冬天

	}
}

虽然代码量增加了,但是代码的可读性变好了,一读函数名就知道要干什么。所以适当的封装函数是有助于代码的可读性的。


5.什么时候使用函数?

  • 当程序有太多相同的代码的时候。
  • 当程序暴露了过多的细节,使可读性变差,就应该使用函数将细节分装起来。


一、函数的本质(重点)

  • 函数的本质就是对象
  • 函数可以作为数据值来赋值
  • 函数作为参数
  • 函数作为返回值
  • 函数可以添加属性和方法

1.1 函数的本质是对象

我们都知道对象是保存在堆内存当中的,在栈内存当中开辟一个房间,房间中保存了指向对象的指针(地址),房间名就是变量名。而函数的本质是对象,函数名 本身保存的就是函数所在 堆内存的地址,也就是那个房间名。

function fn(){}	fn保存的是地址

所以。。。可以将 函数 保存在变量中,变量里存储的就是这个 函数 (对象)的在堆内存的地址。

var add = function fn(){}	这样写是可以,下面4.3.3会讲

上面的那种写法也可以,只不过函数名fn就变成了局部变量了,只能在函数内部调用,一般情况下没什么用,所以我们都会舍弃函数名,对外访问使用变量名。如下:

var add = function(){}
add();				我们可以正常调用函数
console.log(add)	也可以直接获取函数的本体:function(){}

JS 进阶(2) ECMAScript5 — 函数深入理解:函数的本质、函数定义、函数调用(匿函数自执行、js方法链式调用..)、函数参数(arguments..)、函数返回值


ps:使用var定义函数有一个缺点:函数调用只能在函数声明的后面,这就涉及到js预解析的知识。
js预解析的相关知识

错误示例
add();	
var add = function(){}

在预解析阶段会先 函数声明 提升变量声明 提升 ,赋值操作原地等待。所以就相当于

var add;			这里只有add这个变量,该变量的值为underfined

add();				这里却要调用add这个方法,而上面只有add变量,所以会报错	
add = function(){}	到这一步才将变量的地址赋给add

JS 进阶(2) ECMAScript5 — 函数深入理解:函数的本质、函数定义、函数调用(匿函数自执行、js方法链式调用..)、函数参数(arguments..)、函数返回值



1.2 函数作为数据值来赋值

函数的本质是对象
函数要调用就必须加上(),不加()只是把函数当做数据值。

在1.1说过函数的本质是对象,
所以函数可以这样玩:

	var person = {
		name: 'poorpenguin',
		speak: function(){
			alert('hello world!');
		}
	}
	调用函数
	person.speak();

也可以这样玩:

	var arr = [
		'abc',
		function(){
			console.log(1);
		}
	]

	arr[1]();


1.3 函数作为参数

函数的本质是对象
函数要调用就必须加上(),不加()只是把函数当做数据值。
函数作为参数的例子我们很常见,就是定时器这一类的。

	setTimeout(fn,1000);			将fn这个函数作为数据值传入定时器,1秒后将传入的数据值作为函数执行
	function fn(){console.log(1)};
	
	或者
	setTimeout(function(){},1000)

但是一旦在函数名后加上()就表示立即调用了。

错误事例
	setTimeout(fn(),1000);	立即调用fn函数
	function fn(){}


1.4 函数作为返回值

函数的本质是对象
直接上案例

		function fn(){
			return function(){
				console.log(1);
			}
		}
		var newFn = fn();	变量newFn会得到return后面返回的函数体。
		newFn();			调用新函数,控制台打印1

或者

		function fn(){
			return function(){
				console.log(1);
			}
		}
		fn()();		这样也可以


1.5 函数添加属性和方法

函数的本质是对象
所以函数是可以添加属性和方法。 万物皆对象。

		function add(num1,num2){
			return num1+num2;
		}
		add.name = 'poorpenguin';
		add.sex = 'nan';
		add.setSex = function(sex){
			this.sex = sex;
			return this.sex;
		}


		console.log(add.sex);				输出nan	
		console.log(add.setSex('male'));	输出male
		console.log(add.sex);				输出male
		console.log(add(1,2));				3

总结:所以想要深入的了解函数,玩转函数,除了知道函数的本质是对象这一特性外,还要了解JS的解析机制、作用域。
js预解析的相关知识



二、函数的定义

函数有三种定义方式

  • 函数声明
  • 构造函数
  • 赋值表达式

函数定义类型: 函数声明 构造函数 赋值表达式
例子: function fn(){}; var fn = new Function(){}; var fn = function(){};
- 函数声明 赋值语句 赋值语句
特点: 简洁,直观 多此一举,效率低下 简洁,直观
定义位置 js解析阶段会进行函数提升,所以在哪定义都可以 必须在调用语句之前 必须在调用语句之前

2.1 函数声明

function add(num1,num2){
	return num1+num2;
}

fn();

2.2 构造函数(建议不要使用)

注意:形参和语句都是字符串。

var add = new Function('num1','num2','return num1+num2;');
add();

计算21 * 32 + 24 / 3 - 5,就要写一坨代码。

	var add = new Function('num1','num2','return num1+num2;');
	var subtract = new Function('num1','num2','return num1-num2;');
	var multiply = new Function('num1','num2','return num1*num2;');
	var divide = new Function('num1','num2','return num1/num2;');

	var val = subtract(add(multiply(21,32),divide(24,3)),5);
	console.log(val);

2.3 赋值表达式(注意函数调用的位置)

var add = function(num1,num2){
	return num1+num2;
}
add();

一般推荐使用 函数声明 和 赋值表达式 的方式来定义函数



2.4 函数定义的位置

2.4.1 在对象中定义

函数的本质是对象。
除了常见的在全局和局部中定义函数,最常用的就是在对象中定义函数。

可以这样
	var person = {	
		name:'poorpenguin',
		speak:function(){alert('hello world!');}
	}
也可以这样
	var person = {
		name:'poorpenguin'
	}
	person.speak = function(){alert('hello world!');};

这是调用函数
person.speak();

这是获取函数本身,毕竟函数的本质是对象!!!
person.speak;

2.4.2 在局部作用域中定义

函数也是可以在局部作用域,也就是函数作用域中定义的。

在局部作用域中定义函数,我认为唯一的好处就是 不会污染全局空间 吧。

(if和for在ES5中不是块级作用域,别想多了)

例子:判断输入的数字是否合法,并求和

	function add(){
		if(!isNumber(arguments)){
			console.log('请传入数字类型的参数');
		}else{
			return getSum(arguments);
		}


		//判断是否是数字
		function isNumber(arg){
			for(var i=0; i<arg.length; i++){
				if(isNaN(arg[i]))return false;
			}
			return true;
		}
		//求和
		function getSum(arg){
			var sum = 0;
			for(var i=0; i<arg.length; i++){
				sum +=arg[i];
			}
			return sum;
		}
	}

	add('abc',1,2);	输出 请传入数字类型的参数
	add(1,2,3);		输出 6
	add(1,33,56);	输出 90

2.4.3 补充:arguments对象(4.3会讲)

调用函数传入的参数都可以在函数中的arguments对象中找到。

  • arguments对象只是与数组类似,并不是Array实例
  • []语法可以访问它的每一个元素
  • length属性确定传递参数的个数


三、函数的调用

  • 普通函数的调用
  • 匿名函数的调用(自执行)
  • 递归调用(略)
  • 对象属性获取方式(补充,很重要)
  • 对象方法的调用
  • 对象方法的链式调用
  • 构造函数的调用
  • 函数的间接调用

3.1 普通函数的调用

普通函数的调用不多说。


	function fn(){}
	fn();
	
	var fn = function(){}
	fn();

	var fn = function add(){}
	fn();
	


3.2 匿名函数的调用(自执行)

匿名函数的好处

将代码放在匿名函数中,代码所处的环境从全局作用域变为局部作用域,这样就不会污染全局作用域了,在代码模块化的时候就不会和其他的代码冲突。

函数的自执行和匿名函数的自执行是一样的。

前面说过函数的本质就是对象。

观察一下普通函数的调用
	var fn = function(){
			console.log(1);
	}
	fn();

打印变量fn,输出的是函数本体,因此有了一个大胆的想法:在定义完匿名函数以后在后面加个()不就立即调用了吗?

function(){
	console.log(1);
}();	

这个大胆的想法是对的,但是光是这样是行不通的,因为js解析器会把函数声明提前。
js解析器的机制:将以 function开头的函数声明 和 以 var开头的变量声明 提前。

匿名函数的自执行案例

所以利用这个机制,我们不让function这个关键字打头就可以,下面几种酷炫的写法大家可能都见过。
方式一:
(function(){
	....
})();

方式二:
(function(){
	...
}());

方式三:
!function(){
	....
}();

方式四:
~function(){
	....
}();


!+-~function(){	这样都可以
	....
}();


@function(){	这种就是明显想太多了,是错的。

}();

匿名函数传参

    (function(num1,num2){
    	console.log(num1+num2);	控制台输出	79
    })(56,23);

函数的自执行

   		(function add(num1, num2){
   			console.log(num1+num2);
   		})(1,2);


3.3 对象属性获取方式

其实这个不应该放在这里说的,但是我觉得这个很重要。

对象属性值有两种获取方式:

  • 通过.来获取(大家最常用的)
	var person = {
		name: 'poorpenguin',
		sex: 'male',
		speak: function(){
			console.log('hello world!');
		}
	}
	
console.log(person.name);
  • 通过[]来获取 (推荐使用)
    (这种获取方式功能强大,因为[]里面接收的是一个字符串,甚至在[]中进行变量拼接都可以。)
	var person = {
		name: 'poorpenguin',
		sex: 'male',
		speak: function(){
			console.log('hello world!');
		}
	}
	
console.log(person['name']);

举个例子:给person对象添加属性(name: ‘poorpenguin’, age: 18, sex: ‘male’),通过对象中的setPerson方法。
   	var person = {	
   		setPerson: function(property,value){	第一个参数是属性,第二个参数是值
   			this[property] = value;		
   			return this;
   		}
   	}

   	person.setPerson('name','xm').setPerson('age',18).setPerson('sex','male');
   	console.log(person);

JS 进阶(2) ECMAScript5 — 函数深入理解:函数的本质、函数定义、函数调用(匿函数自执行、js方法链式调用..)、函数参数(arguments..)、函数返回值
但是如果改成

	this.property = value;	变成个给person对象中的property 属性添加value值。

结果根本不是我们想要的
JS 进阶(2) ECMAScript5 — 函数深入理解:函数的本质、函数定义、函数调用(匿函数自执行、js方法链式调用..)、函数参数(arguments..)、函数返回值



3.4 对象方法的调用

方法就是对象中的定义的函数

调用自定义的方法
	var person = {
		name: 'poorpenguin',
		speak: function(){
			console.log('hello world!');
		}
	}
	person.speak();

浏览器的事件(方法)我们也可以直接调用。

直接调用点击事件
document.onclick = function(){
	console.log('你点击了文档');
}
document.onclick();		我们可以直接调用该事件,而不用去触发。因此我们可以来模拟鼠标事件。

案例:模拟鼠标点击事件

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>模拟鼠标点击事件</title>
<style type="text/css">
    #box{position: relative; width: 100px; height: 100px; 
         background-color: red; margin: 10px auto;}
    #close{position: absolute; right: 0px; top: -4px; color: yellow;}
    #btn{display: block; width: 100px; margin: 0 auto;}
</style>
</head>
<body>
    <div id="box">
        <span id="close">X</span>
    </div>
    <button id="btn">关闭</button>
<script type="text/javascript">
	var btn = document.getElementById('btn');
   	btn.onclick = function(){
   		document.getElementById('box').setAttribute('style','display:none;');
   	}

   	document.getElementById('close').onclick = function(){
   		btn.onclick();
   	}

</script>
</body>
</html>


3.5 对象方法的链式调用(推荐使用)

学过jquery的应该都知道链式语法,用起来很爽。
其原理就是每次调用完方法都会返回其本身。

$('#divTest').text('Hello,World!').removeClass('blue').addClass('bold').css('color','red');

利用jquery链式调用的原理,在js对象中的方法也是可以实现链式调用的,甚至可以使代码更简洁。

案例1:js方法链式语法

	var tool = {
		add: function(num1,num2){
			console.log(num1+num2);
			return this;
		},
		divid: function(num1,num2){
			console.log(num1-num2);
			return this;
		},
		multiply: function(num1,num2){
			console.log(num1*num2);
			return this;
		},
		subtract: function(num1,num2){
			console.log(num1/num2);
			return this;
		}, 
	}

	tool.add(1,2).divid(5,3).multiply(2,3).subtract(6,3);

JS 进阶(2) ECMAScript5 — 函数深入理解:函数的本质、函数定义、函数调用(匿函数自执行、js方法链式调用..)、函数参数(arguments..)、函数返回值

ps:推荐使用js方法链式语法的这种,但也不要丧心病狂的串上百条。

案例2:

   	var person = {
   		setPerson: function(property,value){
   			this[property] = value;
   			return this;
   		}
   	}

   	person.setPerson('name','xm').setPerson('age',18).setPerson('sex','male');
   	console.log(person);

3.6 构造函数的调用

不管是调用自己写的构造函数,还是内置的构造函数,我们都要使用new关键字。

  • 构造函数必须通过new实例化调用。
  • 构造函数的首字母请务必大写。(和普通函数区分开)

这些都是规范,能遵守的经量遵守。

内置的构造函数
var fn = new Function(...);

var arr = new Array();
   		arr.push('poorpenguin');
   		arr[1] = 123;			这两种都是赋值方式
   		
var obj = new Object();

自己定义的构造函数
function Person(){}

var ps = new Person();

案例1:实例化构造函数和直接调用构造函数的区别

    	function Person(){
    		return 'poorpenguin';
    	}

    	document.write(Person());
    	document.write('<br>');
    	document.write(new Person());

JS 进阶(2) ECMAScript5 — 函数深入理解:函数的本质、函数定义、函数调用(匿函数自执行、js方法链式调用..)、函数参数(arguments..)、函数返回值


案例2:自定义构造函数并实例化一个对象

构造函数
   	function Person(){
   		this.name='poorpenguin';
   		this.sex = 'male';
   		this.speak = function(){
   			console.log('hello world!');
   		};
   	}

	var person = new Person();	实例化对象
	person.age = 18;

	console.log(person);
	console.log(person.speak());

JS 进阶(2) ECMAScript5 — 函数深入理解:函数的本质、函数定义、函数调用(匿函数自执行、js方法链式调用..)、函数参数(arguments..)、函数返回值



3.7 函数的间接调用(call()和apply())

我们上面所有的函数的调用都是直接调用。
函数的间接调用这玩意还算挺重要的,用它我们以后可以实现继承父类的方法,也是 可以写出特别装逼的代码!!!

函数的间接调用是使用call()apply()这两个方法。

  • 前面说了函数的本质就是对象,所有每一个函数(对象)都有call()apply()这两个方法。
  • call()apply()唯一区别就是在它们的传参方式上。
call(obj,arg1,arg1,arg2....);
apply(obj,[arg1,arg2,arg3...]);
这两个方法第一个参数都是一样,传入对象obj,这个对象将会改变调用call和apply这两个方法的函数中this的指向。

obj不写的话,默认是window

《javascript权威指南》对call()apply()的描述
JS 进阶(2) ECMAScript5 — 函数深入理解:函数的本质、函数定义、函数调用(匿函数自执行、js方法链式调用..)、函数参数(arguments..)、函数返回值

call() 例子1:改变this的指向

	var name = '阿西吧';

	var person = {
		name:'poorpenguin',
		speakName: function(){
			console.log(this.name);
		}
	}
	var dog = {
		name : 'back',
	}

	console.log(person.speakName.call());		阿西吧	
	console.log(person.speakName.call(window));	阿西吧
	console.log(person.speakName.call(this));	阿西吧

	console.log(person.speakName.call(person));	poorpenguin
	console.log(person.speakName.call(dog));	back

call() 例子2:传参
当使用call()需要传参的时候,一个obj参数不能省略。

	var fn1 = {
		a: 1,
		add: function(b, c){
			console.log(this.a + b + c);
		}
	}

	var fn2 = {
		a: 2
	}

	console.log(fn1.add.call(fn1,2,3));		1+2+3 = 6
	console.log(fn1.add.call(fn2,2,3));		2+2+3 = 7
	
	apply()和call()唯一的不同就是在传参上面
	apply()只需要传两个参数,第二个参数是个数组
	console.log(fn1.add.apply(fn2,[2,3]));


四、函数的参数

什么可以作为参数?

只要是数据值都可以作为参数(包括函数)。

4.1参数的类型

参数分为形参实参

参数传递的本质是赋值,在预解析阶段,函数的形参的值都是underfined;在语句执行阶段,才会把实参的值赋值给形参。


4.2参数的个数

情况一:实参个数 = 形参个数

这个情况皆大欢喜。

情况二:实参个数 < 形参个数

意思就是在语句执行阶段,有形参没有被赋值,那没有被赋值的形参的值就为underfined;

	function add(num1,num2){
		return num1 + num2;
	}
	console.log(add(3));	num1 = 3; num2 = underfined ;输出的结果为NaN3+underfined)

情况三:实参个数 > 形参个数

可以不用管,也可以使用arguments这个类数组获取所有实参的值。



4.3 arguments

arguments对象中保存了所有传入的实参。

  • 每一个函数都有
  • 是一个局部对象
  • 类数组(不是真正数组,是一个类似数组的对象,所有没有数组的方法)
	function add(num1){
		console.log(arguments);
	}
	console.log(add(23, 345, 1, 54, 12, 56));

JS 进阶(2) ECMAScript5 — 函数深入理解:函数的本质、函数定义、函数调用(匿函数自执行、js方法链式调用..)、函数参数(arguments..)、函数返回值

arguments中的值和参数的值是一一对应的,改变了arguments中的值,对应参数的值也会变。


4.3.1 判断实参的个数是否等于形参的个数

传入实参的个数我们可以使用argument.length来获取。
形参的个数可以使用函数名.length来获取

	function add(num1, num2){
		if(arguments.length != add.length){
			throw new Error('请传入'+add.length+'个参数!');
		}
		return num1+num2;
	}

	console.log(add(1));

JS 进阶(2) ECMAScript5 — 函数深入理解:函数的本质、函数定义、函数调用(匿函数自执行、js方法链式调用..)、函数参数(arguments..)、函数返回值

4.3.2 arguments.callee 指向函数本身

arguments.callee该属性指向函数本身,和函数名是一样的。此属性的作用是使用在递归上。

	function add(num){
		document.write(arguments.callee);
		return num;
	}
	add();

JS 进阶(2) ECMAScript5 — 函数深入理解:函数的本质、函数定义、函数调用(匿函数自执行、js方法链式调用..)、函数参数(arguments..)、函数返回值

如果递归比较复杂,并且我们要修改函数名的话,我们还有修改函数体中的函数名。

这是一个简单的递归,自己调用自己。
	function sum(n){
    	if(n==1) return 1;
    	return sum(n-1) + n;
	}

使用arguments.callee可以解决这个问题,无论怎么修改函数名都可以。

这是一个简单的递归,自己调用自己。
	function sum(n){
    	if(n==1) return 1;
    	return arguments.callee(n-1) + n;
	}

4.3.3 arguments.callee的局限性

可惜的是arguments.callee这个对象在js的 严格模式 下是 禁用 的。
那我们如何解决递归调用中的函数名的问题呢?
在上面1.1函数的本质是对象有提到过

var add = function fn(){}

add作为函数名对外使用,而fn可以作为函数的小名,在函数内部使用。

在严格模式下可以使用函数的小名实现递归。
	var sum = function fn(n){
    	if(n==1) return 1;
    	return fn(n-1) + n;
	}


4.4 建议使用对象作为参数

当函数需要的参数在3个以上的时候,可以考虑使用对象作为参数

调用函数的时候,我们必须要记住参数的顺序。。。
	function setPerson(name,age,tel,addr,sex){
		var person = {};
		person.name = name;
		person.age = age;
		person.tel = tel;
		person.addr = addr;
		person.sex = sex
	}
	setPerson('poorpenguin','25','123213....','中国','male');	

修改使用对象作为参数,就特别灵活,不同考虑实参的顺序。

我们不用去管传入实参的顺序问题了。
	function setPerson(obj){
		var person = {};
		person.name = obj.name || '未知';
		person.age = obj.age || 18;
		person.tel = obj.tel || '123123';
		person.addr = obj.addr || '中国';
		person.sex = obj.sex || 'male';
	}
	
	setPerson({
		name: 'poorpenguin',
		addr: '中国',
		sex: 'male',
		tel: '123123',
		age: 25;
	});

4.5 将函数作为参数

例子…

setTimeout(function(){},1000)


五、函数的返回值

1.return 有两个作用

  1. 结束当前函数
  2. 将值返回

2.什么可以做返回值‘’

只要是数据值都可以作为返回值。对象,布尔,数字,字符串,underfined,null,数组,函数这些都可以。

3.对象作为返回值的一个·小坑

我们正常代码风格是下面这样的
function fn(){
	return {
		name: 'poorpenguin',
		sex: 'male',
	};	
}

但是也有代码风格是这样的
function fn()
{
	return 		这样写报错
	{
		name: 'poorpenguin',
		sex: 'male',
	};	
}

4.函数作为返回值很优雅!

案例1:返回输入数字的四则运算的值

写法一:
	function calculate(num1,num2){
		if(arguments.length != arguments.callee.length){
			throw new Error('请输入'+arguments.callee.length+'实参!');
		}

		return {
			addRes: add(),
			subtractRes: subtract(),
			multiplyRes: multiply(),
			divideRes: divide()
		}


		function add(){
			return num1+num2;
		}
		function subtract(){	
			return num1-num2;
		}
		function multiply(){
			return num1*num2;
		}
		function divide(){
			return num1/num2;
		}
	}

 	console.log(calculate(1,2));

JS 进阶(2) ECMAScript5 — 函数深入理解:函数的本质、函数定义、函数调用(匿函数自执行、js方法链式调用..)、函数参数(arguments..)、函数返回值

写法二:丧心病狂一点
		 function calculate(num1,num2){
			if(arguments.length != arguments.callee.length){
				throw new Error('请输入'+arguments.callee.length+'实参!');
			}

			return {
				addRes: function add(){
					return num1+num2;
				}(),
				subtractRes: function subtract(){	
					return num1-num2;
			    }(),
				multiplyRes: function multiply(){
					return num1*num2;
				}(),
				divideRes: function divide(){
					return num1/num2;
				}()
			}
		}
		console.log(calculate(1,2));
写法三:再丧心病狂一点
   		 function calculate(num1,num2){
   			if(arguments.length != arguments.callee.length){
   				throw new Error('请输入'+arguments.callee.length+'实参!');
   			}

   			return {
   				addRes: function(){
   					return num1+num2;
   				}(),
   				subtractRes: function(){	
   					return num1-num2;
   			    }(),
   				multiplyRes: function(){
   					return num1*num2;
   				}(),
   				divideRes: function(){
   					return num1/num2;
   				}()
   			}
   		}
   		console.log(calculate(1,2));