本章内容
- 函数表达式、函数声明以及箭头函数
- 默认参数及扩展操作符
- 使用函数实现递归
- 使用闭包实现私有变量
0. 走进函数
函数实际上也是对象,每个函数都是Function类型的示例,而Function也有属性和方法,跟其他引用类型一样,因为函数是对象,所以函数名就是指向函数对象的指针,而且不一定与函数本身紧密绑定。函数常以函数声明的方式定义,比如:
1. 函数时式声明函数
function sum(a, b) {
return a + b
}
2.表达式定义函数
- 代码定义了一个变量sum并将其初始化为一个函数。注意function关键字后面没有名称,因为不需要,这个函数可以通过变量sum来引用
let sum=function(a,b){
return a + b
};
sum(1,2)
3.构造函数式定义函数
- 这个构造函数接收任意多个字符串参数,最后一个参数始终会被当成函数体,而之前的都是新函数的参数
let sum=new Function("a","b","return a+b")//不推荐
1. 箭头函数
ECMAScript6新增了适应(=>)语法定义函数表达式的能力,很大程度上,箭头函数实例化的函数对象与正式的函数表达式创建的函数对象行为是相同的。任何可以使用函数表达式的地方,都可以使用箭头函数。
let arrowSum=(a,b)=>{
return a+b;
}
console.log(arrowSum(2,5))
箭头函数的简介语法非常适合嵌入函数的场景
const ints=[1,2,3]
console.log(ints.map((i)=>{return i+1}))//[2,3,4]
如果只有一个参数,那也可以不使用括号,只有没有参数或者多个参数的情况下,才需要使用括号
// 以下两种写法都有效
let double = (x) => { return 2 * x; };
let triple = x => { return 3 * x; };
// 没有参数需要括号
let getRandom = () => { return Math.random(); };
// 多个参数需要括号
let sum = (a, b) => { return a + b; };
// 无效的写法:
let multiply = a, b => { return a * b; };
箭头函数也可以不用大括号,但是这样会改变函数的行为,使用大括号说明包含“函数体”,可以在一个函数中包含多条语句,跟常规的函数一样,如果不适用大括号,那么箭头后面就只能有一行代码,比如一个赋值操作,或者一个表达式,而且,省略大括号会隐式返回这行代码的值。
一下两种写法都有效,而且返回相应的值
let double = (x) => {return 2 * x;}
let triple = (x) => 3 * x;
虽然箭头函数很简洁,但是很多场合不适用。箭头函数不能使用argument、super、new.target,也不能做构造函数,此外,箭头函数也没有prototype属性。
2. 函数名
因为函数名就是指向函数的指针,所以他们跟其他包含对象执行的变量具有相同的行为。这意味着一个函数可以有多个名称,所下所示:
function sum(a,b){
return a+b
}
console.log(sum(1,2))//3
let anotherSum=sum;
console.log(anotherSum(2,3))//5
sum=null;
console.log(anotherSum(3,4))//7
上代码定义了一个名为sum()的函数,英语求两数之和,然后又声明了一个变量anotherSum,并将它的值设置等于sum。注意,使用不带括号的函数名会访问函数指针,而不会执行函数。此时anotherSum和sum都指向同一个函数,调用anotherSum()也可以返回结果。把宿命设置为null之后,就切断了它与函数之间的关联。而anotherSum()还是可以照常使用,没有问题。
3.理解函数
3.1 普通函数中的参数
函数的参数跟大多数语言不同,ECMScript函数既不关心参数的个数,也不关心这些参数的数据类型,定义函数时要接收两个参数,并不意味着调用时就传两个参数,你可以传一个、三个、甚至一个也不传,解析器也不会报错。
之所以会这样,主要是因为ECMAScript函数在参数内部标新啊一个数组,函数被调用时总会接收一个数组,但是函数并不关心这个数组中包含什么,如果数组中什么也没有,那没问题;如果数组的元素超出了要求,那也没问题,事实上,在使用function关键字定义(非箭头)函数时,可以在函数内部访问arguments,从中取得传进来的每个参数值。
arguments对象是一个类数组对象(但不是Array的实例),因此可以使用中括号语法访问其中的元素(第一个参数时arguments[0],第二个参数是arguments[1])。而要确定传进来了多少个参数,可以访问arguments.length属性。
在下面的例子中,sayHi()函数的第一个参数叫name:
function sayHi(name, message) {
console.log("Hello " + name + ", " + message);
}
可以通过arguments[0]取得相同的参数值。因此,把函数重写成不声明参数也可以:
function sayHi() {
console.log("Hello " + arguments[0] + ", " + arguments[1]);
}
使用例子:
function doAdd(num1, num2) {
if (arguments.length === 1) {
console.log(arguments[0] + 10);
} else if (arguments.length === 2) {
console.log(arguments[0] + arguments[1])
}
}
3.2 箭头函数中的参数
如果函数是使用箭头语法定义的,那么传给函数的参数将不能使用arguments关键字访问,而只能通过定义的命名参数访问。
注意:ECMAScript中所有参数都是按值传递的,不可能按引用传递参数,如果对象作为参数传递,那么传递的值就是这个对象的引用。
4. 没有重载
如果定义了两个同名的函数,则后定义的会覆盖先定义的。
5. 默认参数值
在ECMAScript5.1以前,实现默认参数的一种常用的方式就是检测某个参数是否等于undedined,如果是则意味着没有传这个参数,那就给他赋一个值。
function makeKing(name) {
name = (typeof name !== 'undefined') ? name : 'heihei';
return `king ${name} VIII`;
}
console.log(makeKing());//king heihei VIII
console.log(makeKing('huang'))//king huang VIII
ECMAScript6之后就不用这么麻烦了,因为它支持显示定义默认参数了(下面的代码是与前面的等价的ES6写法)
function makeKing(name='heihei') {
return `king ${name} VIII`;
}
console.log(makeKing());//king heihei VIII
console.log(makeKing('huang'))//king huang VIII
使用默认参数时,arguments对象的值不反映参数的默认值,只反映给函数的参数值。
5.1 收集参数
在构思函数定义时,可以使用扩展操作负=符号、把不停长度的独立参数组合成为一个数组。这有点类似arguments对象额构造机制,只不过收集参数的结果会得到一个Array实例。
function getSum(...values){
//顺序累加values中的所有值
//初始值综合为0
return values.reduce((x,y)=>x+y,0)
}
console.log(getSum(1,2,3))//6
如果收集的前面如果还有命名参数,则指挥收集其余参数,如果没有则会得到空数组,因为收集参数的结果可变,所以只能把它作为最后一个参数。
function ignoreFirst(firstValue, ...values) {
console.log(values);//注意这是收集values的值
}
ignoreFirst(); // []
ignoreFirst(1); // []
ignoreFirst(1, 2); // [2]
ignoreFirst(1, 2, 3); // [2, 3]
箭头函数虽然不支持arguments对象,但是支持收集参数的定义方式,因此也可以实现与使用arguments一样的逻辑:
let getSum = (...values) => {
//顺序累加values中的所有值
//初始值综合为0
return values.reduce((x, y) => x + y, 0)
}
console.log(getSum(1, 2, 3)) //6
6. 函数声明与函数表达式
事实上,JavaScript引擎在加载数据时对他们是区别对待的。JavaScript引擎在任何代码执行之前,先回去读函数声明,并在执行上下文中生成函数定义。而函数表达式必须等代码执行到它那一行。
// 没问题
console.log(sum(10, 10));
function sum(num1, num2) {
return num1 + num2;
}
// 会出错
console.log(sum(10, 10));
let sum = function(num1, num2) {
return num1 + num2;
};
7. 每个函数都包含两个非继承得来的方法
- call
- apply
在非严格模式下,当你不需要改变this的指向,但是又想使用apply或者call在该对象中进行其他操作的时候,你可以在apply、call的第一参数传入null、undefined或者window,举例如下:
1.求出给定数组中最大值
- 我们知道Math.max()可以找出最大值,但是其接收的参数形式不是数组,这时候我们可以巧妙利用apply或者call
var maxnum=Math.max(2,45,8,9)
var arr=[22,1,88,85,87,55]
var arrMax=Math.max.apply(null,arr)//以数组的方式传入
console.log(arrMax)
2.将类数组转换为真正的数组
- 按以前可能是遍历一下类数组,然后将其push到新数组中
- 现在便可以利用apply或者call
function changeArr(){
console.log(arguments)
// var a=array.slice();
var arr= Array.prototype.slice.apply(arguments)//实质是使用slice实现数组的浅复制
console.log(arr)
}
changeArr(1,2,3)
3.数组追加
1.const的方式
//可以使用concat
const arr1 = [1, 5, 7]
const arr2 = [48, 8, 8]
console.log(arr1.concat(arr2))
2.使用push结合循环
const arr1 = [1, 5, 7]
const arr2 = [48, 8, 8]
for (let i = 0; i < arr2.length; i++) {
arr1.push(arr2[i])
}
console.log(arr1)
3.直接使用push加apply或者call
const arr1 = [1, 5, 7]
const arr2 = [48, 8, 8]
Array.prototype.push.apply(arr1,[1,5,8,9,40])
Array.prototype.push.call(arr1,1,5,8,9,40)
console.log(arr1)
8.函数中的this
另一个特殊的对象是this,它再标准函数和箭头函数中有不同的行为。
在标准函数中,this引用的是把函数当成方法调用的上下文对象,这时候的通常称其为this值(在网页的全局上下文中调用函数时,this指向windows),来看下面的例子:
注意:函数名只是保存指针的变量,因此全局定义的sayColor()函数和o.sayColor()是同一个函数,只不过执行部的上下文不同。
window.color = "red";
let o = {
color: 'blue'
}
function sayColor() {
console.log(this.color)
}
sayColor() //'red'
o.sayColor = sayColor;
o.sayColor()//blue
定义全局上下文中的函数sayColor()引用了this对象。这个this到底引用哪个对象必须到函数被调用时才能被确定,因此这个值在代码执行的过程中可能会被改变。如果全局上下文调用sayColor(),则this指向window,则this.color=winddow.color。而把sayColor()赋值给o之后再调用o.satColor(),this指向o,即this.color相当于o.color.
在箭头函数中,this引用的是定义箭头函数的上下文。下面的例子演示了这一点。在对sayColor()的两次调用中,this引用都是window对象,因为这个箭头函数是在window上下文中定义的:
window.color='red';
let o={
color:'blue'
}
let sayColor=()=>{
console.log(this.color)
}
sayColor();//red
o.sayColor=sayColor;
o.sayColor();//red
我们知道,在时间回调或者定时回调中调用某个函数时,this指向的并非想要的对象。此时将回调函数写成箭头函数就可以解决问题,因为箭头函数中this会保留定义该函数时的上下文:
9. 函数的属性和方法
前面提到过,ECMScript中的函数是对象,因此有属性和方法,每个函数都有两个属性:length和prototype。其中,length属性保存函数定义的命名参数的个数,如下例所示:
function sayName(name) {
console.log(name);
}
function sum(num1, num2) {
return num1 + num2;
}
function sayHi() {
console.log("hi");
}
console.log(sayName.length); // 1
console.log(sum.length); // 2
console.log(sayHi.length); // 0
遇上代码定义了三个函数,每个函数的命名参数都不一样,sayName函数有一个命名参数,所以其length属性为1.类似的,sum函数有两个命名参数,所以其length属性是2.而sayHi没有命名参数,其length属性为0.
prototype属性允许是ECMScript核心中最有趣的部分,prototype是保存引用类型所有实例方法的地方,这意味着toString()、valueOf()等方法实际上都保存在prototype上,进而由所有实例共享。这个属性在自定义类型时特别重要。在ECMAScript5中,prototype属性是不可枚举的,因此使用for-in循环不会返回这个属性。
函数还有两个方法,apply()和call(),这两个方法都会指定this的值来调用函数,即会设置调用函数时函数体内this对象的值。apply方法节后两个参数:函数内this的值和一个参数数组。第二个参数可以时Array的实例,但也可以时arguments对象、来看下面的例子:
function sum(num1, num2) {
return num1 + num2;
}
function callSum1(num1, num2) {
return sum.apply(this, arguments); // 传入arguments对象
}
function callSum2(num1, num2) {
return sum.apply(this, [num1, num2]); // 传入数组
}
console.log(callSum1(10, 10)); // 20
console.log(callSum2(10, 10)); // 20
在这个例子中,callSum1()会调用sum()函数,将this作为函数体内的this指向传入(这里等于window,因为是在全局作用域中调用的),同时还传入arguments对象。callSum2()也会调用sum函数,但是会传入参数的数组,着两个函数都会执行并保存正确的结果。
注意:在严格模式下,调用函数时如果没有指定上下文对象,则this值不会指向window,除非使用apply()或者call()把函数指定给一个对象,否则this的值会变成undefined。
call()方法与apply()的作用一样,只是传参的形式不同。死一个参数跟apply()一样 ,也是this值,而剩下的要传给被调用函数的参数则是逐个传递的。换句话说,通过call()向函数传参,必须将参数一个一个的列出来。比如:
function sum(num1, num2) {
return num1 + num2;
}
function callSum(num1, num2) {
return sum.call(this, num1, num2);
}
console.log(callSum(10, 10)); // 20
到底时使用apply还是call,完全取决于怎么给调用的函数传参更方便,如果想直接传arguments对象会在一个数组,那就用apply;否则,就用call。当然,如果不用给被调用的函数传参,则使用哪个方法都一样。
apply和call真正强大的地方并不是给函数传参,而是控制函数调用上下文即函数体内的this值得能力。
ECMAScript5中处于同样目的定义了一个新的方法bind,bind()方法会创建一个新的函数实例,其this值会被绑定到传给bind()的对象,比如
window.color="red";
var o={
color:'blue'
}
function sayColor(){
console.log(this.color)
}
let objSayColor=sayColor.bind(o)
objSayColor()//blue
在这里,在sayColor上调用bind并传入对象o,创建了一个新函数 objSayColor(), objSayColor()中的this会被设置为o,因此直接调用这个函数,即是是在全局作用域中调用,也会返回字符串"blue"
对函数而言,继承的方法toLocaleString()和toString()始终返回函数的代码。返回的代码的具体格式因浏览器而异。有的返回源代码,有的返回代码的内部形式......
10.函数表达式
let functionName = function(arg0, arg1, arg2) { // 函数体 };
函数表带是看起来像一个普通的变量定义个赋值,即创建一个函数再把它赋值给一个变量functionName,这样创建的函数叫做匿名函数,因为function关键字后面没有标识符,未赋值给其他变量的匿名函数的name属性是空字符串。
函数表达式跟jsjavascript中的其他表达式一样,需要先赋值再使用。下面的例子会导致错误:
sayHi(); // Error! function doesn't exist yet
let sayHi = function() {
console.log("Hi!");
};
理解函数声明与函数表达式之间的区别,关键是理解提升。比如,以下代码可能会出乎意料:
if(condition){
function sayHi(){
console.log('Hi');
}
}else{
function sayHi(){
console.log('Yo!')
}
}
这段代码看起来很正常,就是如果condition为true,则使用第一个sayHi()定义;否则,就是用第二个,事实上,这种写法在ECAMScript中不是有效的语法。JavaScript引擎会尝试将其纠正为合适的声明。问题在于浏览器纠正这个问题的方式不一致。多数浏览器会忽略condition直接返回第二个声明。这种写法很危险,不要使用,不过。如果把上面的函数声明换成函数表达式就没有问题了。
let sayHi;
if (condition) {
sayHi= function () {
console.log('Hi');
}
} else {
sayHi= function () {
console.log('Yo!')
}
}
这个例子如预期一样,根据condition的值为变量sayHi赋予相应的函数。创建函数并赋值给变量的能力也可以用于一个函数中把另一个函数当作返回值。
11. 递归
递归函数通常形式是一个函数通过名称调用自己,如下面的的例子:
function factorial(num){
if(num<1){
return 1;
}else{
return num*factorial(num-1)
}
}
这是经典的递归阶乘函数,虽然这样写是可以,但是把这个函数赋值给其他变量,就会出问题:
function factorial(num){
if(num<=1){
return 1;
}else{
return num*factorial(num-1)
}
}
let anotherFactorial=factorial;
factorial=null;
console.log(anotherFactorial(4))//报错
这里把factorial()函数保存在了另外一个变量anotherFactorial中,然后将factorial设置为null,于是只保留了一个对原始值的引用。而在调用anotherFactorial()时,要递归调用factorial(),但因为它已经不是函数了,所以会报错,在写递归函数时使用arguments.callee可以避免这个问题。
arguments.callee就是一个指向正在执行的函数的指针,因此可以在函数内部递归掉用,如下所示:
function factorial(num){
if(num<=1){
return 1;
}else{
// return num*factorial(num-1)
return num*arguments.callee(num-1)
}
}
将函数名称替换成arguments.callee,可以确保无论同通过什么变量调用这个函数都不会出问题。因此在编写递归函数时,arguments.callee时引用当前函数的首选。
不过,在严格模式下运行的代码不能访问arguments.callee的,因此访问会出错。此时,可以使用命名函数表达式达到目的。比如:
const factorial = (function f(num) {
if (num <= 1) {
return 1;
} else {
return f(num - 1)
}
})
这里创建了一个命名函数表达式f(),然后将它赋值给了变量factorial,即使把函数赋值给另一个变量,函数表达式的名称也不会变,因此递归调用也不会出现问题。这个模式在严格模式和非严格模式下都可以使用。
12. 闭包
匿名函数将此被人误认为时闭包,闭包指的是那些引用了另一个函数作用域变量的函数,通常时在嵌套函数中实现的。比如:
function createComparisonFunction(propertyName) {
return function (object1, object2) {
let value1 = object1[propertyName];
let value2 = object2[propertyName];
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
};
}
这里value1和value2是位于内部函数(匿名函数)中,其中引用了外部函数的变量propertyName。在这个内部函数被返回并在其他地方被使用后,它仍然引用着这个变量,这是因为内部函数的作用域链包含createComparisonFunction()函数的作用域。要理解为什么会这样,可以想想第一次调用这个函数时会发生什么。
在调用一个函数时,会为这个函数调用创建一个执行上下文,并创建一个作用域链。然后用arguments和其他命名参数来初始化这个函数的活动对象。外部函数的活动对象是内部函数作用域链上的第二个对象。这个作用域链一直向外串起了所有包含函数的活动对象,直到全局执行上下文才终止。
这里定义的compare()函数时在全局上下文中调用的。第一次调用compare()时它会创建一个包含arguments、value1、value2的活动对象,这个对象是其作域链上的第一个对象,而全局上下文的变量对象则是compare()作用域链上的第二个对象,其中包含this、result和compare
13. this对象
在闭包中使用this会让代码变复杂,如果内部函数没有使用箭头函数定义,则this对象会在运行时绑定到执行函数的上下文。如果在全局函数中调用,则this在非严格模式下等于window,在严格模式下等于undefined,如果作为某个对象的方法嗲用,则this等于这个对象。匿名函数在这种情况下不会绑定到某个对象,这就意味着this会指向window,除非在严格迷失下this时undefined。不过,由于闭包的写法所致,这个事实有时候没有那么容易看出来,来看下面这个例子:
window.identity = "the window";
let object = {
identity: 'my obj',
getIdentityFunc() {
return function () {
return this.identity;
}
}
}
console.log(object.getIdentityFunc())//函数体
console.log(object.getIdentityFunc()())// "the window"
这里先创建了一个全局变量identity,之后又创建一个包含identity属性的对象。这个对象中包含以一个getIdentityFunc()方法,返回一个匿名函数,这个匿名函数返回this.identity。因为getIdentityFunc()返回函数,所以object.getIdentityFunc()()会立即调用这个返回函数,从而得到一个字符串,可是,此时返回的字符串时"the window",即全局变量identity的值。为什么匿名函数没有使用其包含作用域(getIdentityFunc())的this对象呢?下面将解析:
我们知道,每个函数在被调用的时候都会自动创建两个特殊的变量:this和arguments。内部函数永远不可能访问外部函数的这个两个变量(this和arguments)!!!!。所以它只能向上查找从而找到window。 但是,如果把this保存到闭包可以访问的另一个变量中(如let that=this),则是行的通的,可以堪称是内部函数可以访问外部函数的属性变量。比如:
window.identity = "the window";
let object = {
identity: 'my obj',
getIdentityFunc() {
let that=this
return function () {
return that.identity;
}
}
}
console.log(object.getIdentityFunc()())// "my obj"
这里使用了let that=this展示了与前面例子的区别,在定义匿名函数之前,先把外部函数的this保存到变量that中,然后在定义闭包时,就可以让它访问that,因为这是包含函数中没有任何冲突的变量。即使在外部函数返回后,that仍然指向object,所以调用object.getIdentityFunc()()就返回"my obj"
注意:this和arguments都是不能直接在内部函数中访问的。如果想访问包含作用域中的arguments对象,则同样需要将其引用先保存到闭包能访问的另一个变量中。
14. 立即调用的函数表达式
立即调用的匿名函数又被成为立即调用的函数表达式。它类似于函数声明,但由于被包含在括号中,所以会被解释为函数表达式。紧跟在第一组括号后面的第二组货号就会立即调用前面的函数表达式,下面是一个简单的例子:
(function () {
//块级作用域
})
15 小结
- 函数是JavaScript编程中最有用也最通用的工具。ECMAScript 6新增了更加强大的语法特性,从而让开发者可以更有效地使用函数。
- 函数表达式与函数声明是不一样的。函数声明要求写出函数名称,而函数表达式并不需要。没有名称的函数表达式也被称为匿名函数。
- ES6新增了类似于函数表达式的箭头函数语法,但两者也有一些重要区别。
- JavaScript中函数定义与调用时的参数极其灵活。arguments对象,以及ES6新增的扩展操作符,可以实现函数定义和调用的完全动态化。
- 函数内部也暴露了很多对象和引用,涵盖了函数被谁调用、使用什么调用,以及调用时传入了什么参数等信息。
- JavaScript引擎可以优化符合尾调用条件的函数,以节省栈空间。 闭包的作用域链中包含自己的一个变量对象,然后是包含函数的变量对象,直到全局上下文的变量对象.
- 通常,函数作用域及其中的所有变量在函数执行完毕后都会被销毁。
- 闭包在被函数返回之后,其作用域会一直保存在内存中,直到闭包被销毁。
- 函数可以在创建之后立即调用,执行其中代码之后却不留下对函数的引用。
- 立即调用的函数表达式如果不在包含作用域中将返回值赋给一个变量,则其包含的所有变量都会被销毁。
- 虽然JavaScript没有私有对象属性的概念,但可以使用闭包实现公共方法,访问位于包含作用域中定义的变量。
- 可以访问私有变量的公共方法叫作特权方法。 特权方法可以使用构造函数或原型模式通过自定义类型中实现,也可以使用模块模式或模块增强模式在单例对象上实现。