一、前言
记得最开始学前端的时候,在JavaScript中学的第一类知识就是函数了...这里将学习的感觉分享出来!
说明: 函数其实是一种特殊的对象,每一个函数都是Function
的实例,实例存在自己的属性和方法,其次由于函数是对象,那么函数名就是获取函数对象的一种方式了,函数通常以函数声明的方式
定义,当然也有函数表达式
,几乎是等价
的,这两种是最常见的,还存在一种使用Function构造函数
来实例化,接收多个字符串参数,最后一个字符串参数作为函数体,前面的所有字符串参数会作为函数的参数
js
// 函数声明
function sum(num1, num2) {
return num1 + num2;
}
js
// 函数表达式
let sum = function(num1, num2) {
return num1 + num2;
};
js
// Function构造函数
let sum = new Function("num1", "num2", "return num1 + num2");
二、箭头函数
说明: 这是ECMAScript6新增的行为,能够使用=>
来定义函数表达式,任何可以使用函数表达式的地方,都可以使用箭头函数,=>
后的{}
表示包含函数体,可以在一个函数中包含多条语句,跟常规的函数一样,其规定的写法是() => {}
js
// 基本使用
let arrowSum = (a, b) => {
return a + b;
};
let functionExpressionSum = function (a, b) {
return a + b;
};
console.log(arrowSum(5, 8));
console.log(functionExpressionSum(5, 8));
js
// 作为嵌套函数使用
let ints = [1, 2, 3];
console.log(
ints.map(function (i) {
return i + 1;
})
);
console.log(
ints.map((i) => {
return i + 1;
})
);
注意:
- 如果
只有
一个参数,可以省略()
- 如果
=>
后面只有一句代码时,可以同时
省略{}和return
- 箭头函数不存在自己的
this
,它的this
与其父作用域的this
指向相同- 在只有一个参数的时候如果有使用
默认值
,则()
不能省略
js
// 只有一个参数的时候,=>后只有一句代码
let triple = x => return 3 * x;
三、函数名
说明: 因为函数名是指向函数的一种方式,如果其它的变量也包含指向这个函数的方式,那它们的行为是相同的,也就是说一个函数可以存在多个名称,这个名称可以通过name
属性获取,属性值为保存的变量名,如果是匿名函数则值为""
,如果是通过Function实例
生成,则属性值为anonymous
js
function sum(num1, num2) {
return num1 + num2;
}
let anotherSum = sum;
console.log(sum(10, 10));
console.log(anotherSum(10, 10));
由于函数名在这里可以看作变量,那么函数可以在任何使用变量的地方使用函数,也就是说
函数可以当做值,也可以做为值被函数返回
四、参数
说明: ECMAScript函数既不关心传入的参数个数,也不关心这些参数的数据类型, 也就是你在写函数的时候定义了两个参数,不意味着你必须传两个参数,你可以传一个,也可以一个不传,编辑器都不会报错,因为参数在函数内部表现为一个数组在使用 function关键字定义(非箭头)函数时,可以在函数内部访问arguments
对象,从中取得传进来的每个参数值,这个对象是一个类数组对象,有length
属性,也可以通过[]
访问其内部元素,这个对象里面的值与函数参数是对应
的
js
function sayHi(name,message) {
console.log(arguments);
}
sayHi("zhangsan","shigetiancai")
注意:
() => {}
不存在arguments
对象- 所有参数都是
按值传递
的,如果参数是一个对象,那么传递的值就是这个对象的引用arguments
对象不会反应函数的默认值,只会显示实际传入函数的参数值
五、没有重载
说明: 重载是指可以定义多个同名函数,只要函数的签名不同就可以,由于ECMAScript函数没有签名,因为参数是由包含零个或多个值的数组表示的。没有函数签名,自然也就没有重载,其次函数名可以理解为指针
,那么写同名函数就会导致后面的会覆盖前面的指针,也就定义不了多个同名函数了
js
let addSomeNumber = function (num) {
return num + 100;
};
addSomeNumber = function (num) {
return num + 200;
};
let result = addSomeNumber(100); // 300
六、默认值
说明: ECMAScript6之前设置默认值需要使用typeof
检测是不是undefined然后通过?:
来设置,而ECMAScript6之后可以通过参数 = 值
来设置默认值,值默认是undefined,所以不传的作用是一样的,函数的默认值也可以使用调用函数返回的值
- 函数的默认参数只有在函数
被调用
时才会求值,不会在函数定义时求值。- 计算默认值的函数 只有在调用函数但
未传
相应参数时才会被调用
js
let romanNumerals = ["I", "II", "III", "IV", "V", "VI"];
let ordinality = 0;
function getNumerals() {
return romanNumerals[ordinality++];
}
function makeKing(name = "Henry", numerals = getNumerals()) {
return `King ${name} ${numerals}`;
}
// 不传,会计算默认值
console.log(makeKing());
// 都传,不会得到默认值
console.log(makeKing("Louis", "XVI"));
1.默认值作用域
说明: 给多个参数定义默认值实际上跟使用let关键字顺序声明变量一样,默认参数会按照定义它们的顺序依次被初始化,所以
后定义默认值的参数可以引用先定义的参数,但是
前面定义的参数不能引用后面定义的,同时
参数也存在于自己的作用域中,它们不能引用函数体的作用域
js
function makeKing(name = "Henry", numerals = "VIII") {
return `King ${name} ${numerals}`;
}
<=> 这两个函数效果是一致的
function makeKing() {
let name = "Henry";
let numerals = "VIII";
return `King ${name} ${numerals}`;
}
js
// 后定义可以使用先定义
function makeKing(name = "Henry", numerals = name) {
return `King ${name} ${numerals}`;
}
// 先定义不可以使用后定义
function makeKing(name = numerals, numerals = "VIII") {
return `King ${name} ${numerals}`;
}
// 参数不能使用函数体内的变量
function makeKing(name = "Henry", numerals = defaultNumeral) {
let defaultNumeral = "VIII";
return `King ${name} ${numerals}`;
}
七、参数扩展与收集
说明: ECMAScript6新增了...
操作符,它可以非常简洁地操作和组合集合数据,主要在函数参数列表中使用,此时既可以用于调用函数时传参,也可以用于定义函数参数
1.扩展参数
说明: 对可迭代对象应用扩展操作符,并将其作为一个参数传入,可以将可迭代对象拆分,并将迭代返回的每个值单独传入,所以对于那些需要数组中每个元素依次传入的时候很有用,这种情况可以与arguments
对象结合使用,不过这个对象并不知道...
的存在,只是按照调用函数时传入的参数接收每一个值
js
let values = [1, 2, 3, 4]
console.log(...values)
js
let values = [1, 2, 3, 4];
function countArguments() {
console.log(arguments.length);
}
countArguments(-1, ...values);
countArguments(...values, 5);
countArguments(-1, ...values, 5);
countArguments(...values, ...[5, 6, 7]);
2.收集参数
说明: 在定义函数的时候,可以使用...
操作符把不同长度的独立参数组合为一个真实
数组,收集参数的前面如果
还有命名参数,则只会收集其余的参数;如果
没有则会得到空数组。因为收集参数的结果可变,所以只能
把它作为最后一个参数,它的使用与arguments
对象的使用不冲突
js
function getSum(...values) {
return values;
}
console.log(getSum(1, 2, 3));
js
// 收集参数只能作为最后一个参数
function getProduct(...values, lastValue) {}
function ignoreFirst(firstValue, ...values) {
console.log(values);
}
ignoreFirst();
ignoreFirst(1);
ignoreFirst(1,2);
ignoreFirst(1,2,3);
八、函数声明与函数表达式
1.函数声明提升
说明: 这是函数声明的函数的特点,也就是函数声明会在代码执行之前获得定义。这意味着函数声明可以出现在调用它的代码之后
js
console.log(sum(10, 10));
function sum(num1, num2) {
return num1 + num2;
}
js
// 上面代码在运行的时候应该是这样的,所以不会报错
let sum
console.log(sum(10, 10));
sum = function (num1, num2) {
return num1 + num2;
}
2.函数表达式
说明: function
关键字后面没有标识符的函数称为匿名函数
,也就是这个函数没有名字,所以其name
属性是""
,那么函数表达式可以理解为先创建一个匿名函数,然后将其赋值给一个变量,这种函数不存在声明提升,只有代码运行到的时候才会运行,所以下面这里会报错
js
sayHi();
let sayHi = function() {
console.log("Hi!");
};
九、关于函数
1.arguments
说明: 这里介绍一个arguments
对象中的callee
属性,它是一个指向 arguments对象所在函数的指针,在某些情况下十分有用,看下面这个例子
js
// 这是一个递归函数,不过它正确使用的前提是函数名必须是
// factorial,从而导致了紧密耦合
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
js
// 用arguments.callee代替,如此让函数逻辑与函数名解耦,
// 这样无论函数名是什么都可以调用这个函数,而不是将其
// 函数名定为factorial
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
2.this
说明: 在标准函数里面,一般谁调用这个函数this指向谁,所以在全局作用域中调用函数,this会指向window
js
window.color = "red";
function sayColor() {
console.log(this.color);
}
// 这里调用相当于window.sayColor(),谁调用指向谁,所以this == window
sayColor();
let o = {
color: "blue",
sayColor: function () {
console.log(this.color);
},
};
// 这里是对象o调用,this == o,那么o.color就是其内部的属性了
o.sayColor();
3.new.target
说明: 这个属性是为了检测函数是否是通过new
关键字创建的,如果不是
,则取值为undefined,如果是
,则值为被调用的构造函数
js
function King() {
if (!new.target) {
console.log(new.target);
}
console.log(new.target);
}
new King();
King();
十、改变函数this指向
说明: 这里有三个方法可以做这件事,分别是:apply(函数内this的值,Array实例 / arguments对象)
、call(函数内this的值,参数1,参数2...)
、bind(函数内this的值,参数1,参数2...)
,它们都可以以指定的this值来调用函数
注意:
bind()
会创建一个新的函数实例
js
window.color = "red";
function sayColor() {
console.log(this.color);
}
let o = {
color: "blue",
sayColor: function () {
console.log(this.color);
},
};
sayColor();
sayColor.call(this);
sayColor.call(window);
sayColor.call(o);
js
window.color = "red";
var o = {
color: "blue",
};
function sayColor() {
console.log(this.color);
}
let objectSayColor = sayColor.bind(o);
objectSayColor();
十一、递归
说明: 递归函数通常的形式是一个函数通过名称调用自己,以求和1+2+3+...+n
为例(这里以递归实现)
递归函数的组成部分:
终止条件:
它用于终止递归的执行,一般等函数执行到某种条件的时候,递归函数不再调用自身,而是返回一个确定的值,然后从这个确定的值往回推导,推导出自己需要的结果,一般都是当值为0或者1时能够得到一个确定的值循环体:
用于调用自身的情况,在递归情况下,递归函数通过调用自身并传递更小的输入来解决原始问题的一个或多个子问题,从而将复杂的问题简单化,便于推导结果缩小范围:
递归函数的输入参数应该朝着基本情况靠近。否则,递归函数可能会进入无限循环,永远无法达到自己规定的终止条件
思路: 按照上面的组成,输入的参数需要缩小范围,那就将1+2+3+...+n
变成n+(n-1)+(n-2)+...+1
,这样就可以看出输入参数的范围在依次-1
,然后每相邻的两项都可以写成n + (n - 1)
,因为后一项总比前面多1,然后当第n
项也就是最后一项(从前往后也就是第一项)的时候,值为1,也就是n === 1
,返回的值是1,看图(来自《hello 算法》)
js
function recur(n) {
// 终止条件:也就是第一项的值为1
if (n === 1) {
return 1;
}
// 循环体:每次都会计算 n + (n - 1)的值
// 范围缩小:由于上面每次都会讲参数 - 1,
// 这也起到输入参数范围缩小的作用了
return n + arguments.callee(n - 1);
}
注意: 这里使用
arguments.callee
而不使用recur
是为了递归函数可以赋值给其它变量并且能够继续使用
十二、闭包
说明: 当一个函数引入了另一个函数作用域中的变量时
,这个函数就是一个闭包,其组成如下:
js
// 外函数
function fn1() {
// 数据
let a = 1;
// 内函数并返回
return function fn2() {
// 在内函数中将数据返回出去
return a;
};
}
// 执行闭包函数获取内函数,执行内函数得到返回的数据
let data = fn1();
注意:
- 闭包函数比普通函数更消耗内存,尽量少的去使用
- 如果某个闭包不再使用的话,应该将内函数设置为
null
,使其能够被垃圾回收,避免内存泄露
十三、立即执行函数
说明: 正确名字叫立即调用的函数表达式,也就是这种函数会立即执行,由于其内部是跨级作用域,那么在循环的时候跟使用let
定义变量所得到的效果是一致的了,所以常用于锁定参数
,函数的格式如下:
js
(function () {
// 块级作用域
})();
js
// 假设需要给每个div加上点击事件,在点击的时候确认
// 点击的是第几个,就可以使用这种函数了
let divs = document.querySelectorAll("div");
for (var i = 0; i < divs.length; ++i) {
divs[i].addEventListener(
"click",
(function (frozenCounter) {
return function () {
console.log(frozenCounter);
};
})(i)
);
}
十四、私有变量
说明: 任何定义在函数或块中的变量,都可以认为是私有
的,因为在这个函数或块的外部无法访问其中的变量,不过可以通过闭包来创建访问这些变量的方法,这个方法叫特权方法
,它是能够访问函数私有变量(及私有函数)的公有方法,在对象上创建有两种方式:构造函数创建
和私有作用域创建
1.构造函数创建
说明: 把所有私有变量和私有函数都定义在构造函数中。然后,再创建一个能够访问这些私有成员的特权方法。这样做之所以可行,是因为定义在构造函数中的特权方法其实是一个闭包,它具有访问构造函数中定义的所有变量和函数的能力。
js
function Person(name) {
this.getName = function () {
return name;
};
this.setName = function (value) {
name = value;
};
}
let person = new Person("Nicholas");
console.log(person.getName()); // 'Nicholas'
person.setName("Greg");
console.log(person.getName()); // 'Greg'
缺点:
每次调用构造函数都会重新创建一套变量和方法
2.静态私有变量
说明: 通过使用私有作用域定义私有变量和函数来实现
js
(function () {
// 私有变量
let privateVariable = 10;
// 私有函数
function privateFunction() {
return false;
}
// 构造函数:
// 函数表达式不会创建内部函数
// 不使用关键字声明则这个变量变成全局变量
MyObject = function () {};
// 公有和特权方法
MyObject.prototype.publicMethod = function () {
privateVariable++;
return privateFunction();
};
})();
私有变量和私有函数是由实例共享的。因为特权方法定义 在原型上,所以同样是由实例共享的。特权方法作为一个闭包,始终引用着包含它的作用域,这样所有属性和方法都是
共享
的,修改一个实例的值,所有实例都会得到更改