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(){}
ps:使用var定义函数有一个缺点:函数调用只能在函数声明的后面,这就涉及到js预解析的知识。
js预解析的相关知识
错误示例
add();
var add = function(){}
在预解析阶段会先 函数声明 提升 再 变量声明 提升 ,赋值操作原地等待。所以就相当于
var add; 这里只有add这个变量,该变量的值为underfined
add(); 这里却要调用add这个方法,而上面只有add变量,所以会报错
add = function(){} 到这一步才将变量的地址赋给add
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);
但是如果改成
this.property = value; 变成个给person对象中的property 属性添加value值。
结果根本不是我们想要的
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);
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());
案例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());
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()
的描述
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 ;输出的结果为NaN(3+underfined)
情况三:实参个数 > 形参个数
可以不用管,也可以使用arguments
这个类数组获取所有实参的值。
4.3 arguments
arguments对象中保存了所有传入的实参。
- 每一个函数都有
- 是一个局部对象
- 类数组(不是真正数组,是一个类似数组的对象,所有没有数组的方法)
function add(num1){
console.log(arguments);
}
console.log(add(23, 345, 1, 54, 12, 56));
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));
4.3.2 arguments.callee 指向函数本身
arguments.callee该属性指向函数本身,和函数名是一样的。此属性的作用是使用在递归上。
function add(num){
document.write(arguments.callee);
return num;
}
add();
如果递归比较复杂,并且我们要修改函数名的话,我们还有修改函数体中的函数名。
这是一个简单的递归,自己调用自己。
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 有两个作用
- 结束当前函数
- 将值返回
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));
写法二:丧心病狂一点
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));