基础

在js中,函数其实是对象。每个函数都是Function类型的实例,Function也有自己的属性和方法。因为函数是对象,所以函数名其实就是指向对象的指针,甚至和这个对象没有强制绑定。

定义函数的一些方式:

直接定义

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

匿名定义,定义一个函数,然后把函数指针给sum变量

let sum = function(num1, num2)
{
return num1 + num2;
};

通过箭头,可以称作箭头函数。这是一种语法糖,实际上和上面那个作用是一样的

let sum (num1, num2) => { return num1 + num2;};

箭头函数可以用在任何使用函数表达式的地方(不需要函数名的地方),但是箭头函数和普通函数还是有一些微妙的差别的,例如不能使用arguments等。

最后一种方法时使用Function构造函数,这个函数接收任意多个参数,最后一个参数会被当成函数体,例如let sum = new Function("num1", "num2", "return num1 + num2");.但是这种方法不推荐使用,因为他会被解释两次:第一次是解析new,第二次是解析函数。

函数和var类似,都会自动把声明提升到顶部,因此下列代码是可以的

console.log(sum(10, 10));
function sum(num1, num2)
{
return num1 + num2;
}

函数名和参数

因为函数名是指向函数的指针,所以他们和其他对象指针相同,这样就意味着函数可以有多个名称。

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

consol.log(sum(10, 10));//20

let anothersum = sum;
console.log(anothersum(10, 10));/20

所有函数对象都只会暴露一个只读的name属性,包含函数的信息。多数情况下就是包含一个函数标识符,也就是函数名。如果它是使用构造函数创建的,则会被标识成‘anonymous’

如果函数是get、set或者使用bind()实例化,那么标识符前面会加上一个前缀

function foo(){}
console.log(foo.bind(null).name);//bound foo
let dog =
{
years: 1,
get age()
{
return this.years;
}
set age(newAge)
{
this.years = newAge;
}
}
let propertyDescriptor = Object.getOwnPropertyDescriptor(dog, 'age');
console.log(propertyDescriptor.get.name);//get age
console.log(propertyDescriptro.set.name);//set age

参数

ECMAScript函数的参数和大多数语言不同,它不关心传入参数的个数。定义函数时要接受两个参数,但是实际上可以传一个、两个、三个或者一个不传,都不会报错。

函数的参数在内部表现为一个数组,,函数调用时总会接收这个参数数组,但是数组中有什么其实并不关心。并且在使用function定义非箭头函数时,可以在函数内部访问arguments对象,从中获得传进来的每个参数值。

arguments对象类似于数组,可以通过arguments[0]获得第一个参数,arguments[1]获得第二个参数。可以使用arguments.length获得数组的长度

function sayHi()
{
console.log('Hello ' + arguments[0] + ", " + arguments[1]);
}
sayHi('你', '好');

这和c语言中main函数的参数类似,length相当于argc,而这个数组相当于argv

我们可以修改arguments参数的值甚至添加元素,但是arguments是根据传入时参数的个数决定的,不会再改变

fucntion add(num1, num2)
{
arguments[1] = 10;
console.log(arguments[0] + num2);
}

console.log(add(1, 3);//11

但是在严格模式下即使把arguments的值改变了参数的值也不会改变

箭头函数中的参数

箭头函数中的参数不能使用arguments进行访问,只能使用参数名进行访问

function foo()
{
console.log(arguments[0]);
}
foo(5);//5

let bar = ()=>{console.log(arguments[0]);};
bar(5);//ReferenceError

js的函数没有重载,因为参数都是不确定的。在这种情况下,后命名的函数会覆盖前面一个函数

默认参数与扩展参数

例如:

function makeKing(name = 'henry')
{
return `King ${name} VIII`;
}
console.log(makeKing('Louis'));
console.log(makeKing();//King henry VIII

传undefined相当于没有传值,但是通过传undefined可以部分填充
function makeKing(name = 'Henry', numerals = 'VIII')
{
return `King ${name} ${numerals}`;
}
console.log(makeKing(undefined, 'VI');

在使用默认参数时,arguments不会反映默认参数,他只会保存外来参数。

默认参数还可以直接传一个函数,并且在函数调用时相应的参数函数才会求值,例如:

let num = ['I', 'II', 'III'];
let ordinality = 0;
function getNumerals()
{
return num[ordinality++];
}

function makeKing(name = 'Henry', numerals = getNumerals())//使用函数
{
...
}

默认参数的定义也是有先后顺序的,后面定义的可以引用前面定义的。例如:

function makeKing(name = 'Henry', numerals = name)//引用name
{
return `King ${name} ${numerals}`;
}

它类似于
function makeKing(name, numerals)
{
let name = 'Henry';
let numerals = name;
return `King ${name} ${numerals}`;
}

扩展参数

扩展参数也就是不定长参数,它在不同环境下有不同的用法。

  • 传入参数

    当参数是一个数组时,实际上它的长度是知道的,因此它的作用是将数组中的元素进行拆分,每个元素都是输入的一个参数,可以使用arguments进行访问。

    let values = [1, 2, 3, 4];
    function getSum()
    {
    let sum = 0;
    for(let i=0; i<arguments.length; i++)
    {
    sum += arguments[i];
    }
    return sum;
    }

    可以使用
    console.log(getSum(...values));//10
    console.log(getSum(1, ...values)); //11
    console.log(getSum(1, ...values, 2));//13
  • 输入不定长参数
    例如

    function getSum(...values)
    {
    return values.reduce((x, y) => x+y, 0);
    }
    console.log(getSum(1, 2, 3));//6

    通过这种方法得来的values是一个数组,可以通过数组访问其中的每个输入参数,这种方法可以用于箭头函数(箭头函数中不能用arguments),并且在arguments中每个参数还是独立的,上面arguments.length输出为3

    不定长参数只能放在最后

    function getProduct(...values, lastValue){}//错误
    function getProduct(firstValue, ...values){}//可以

    函数的参数与属性

参数

arguments

前面已经使用过很多次arguments属性了,它代表传进来的参数。除了这个作用外,arguments其实还包含一个callee属性,是一个指向所在函数的指针。

this

在标准函数中,this引用的是调用函数的上下文对象,例如:

window.color = 'red';
let o =
{
color: 'blue';
}
function sayColor()
{
console.log(this.color);
}

sayColor();//red,这时它的上下文对象时window
o.sayColor = sayColor;
o.sayColor();//blue

而在箭头函数中,this引用的是定义时的上下文

window.color = 'red';
let o =
{
color: 'blue';
}

let sayColor = () => console.log(this.color);

sayColor();//red
o.sayColor = sayColor;
o.sayColor();//red,因为是在window中定义的

函数名只是保存指针的变量,实际上函数都在代码区,只不过执行的上下文不同

caller

caller引用调用该函数的函数,例如

function outer()
{
inner();
}

function inner()
{
console.log(inner.caller);
}

outer();

结果返回outer的代码,因为inner.caller是outer

new.target

new.target是为了检测这个函数是作为创建新对象而new的还是作为普通函数,如果是作为普通函数,那么new.target值是undefined,如果是new,那么将引用被调用的函数

function King()
{
if(!new.target)
{
throw `King must be instantiated using "new"`
}
console.log(`King instantiated using "new"`);
}

new King();//King instantiated using "new"
King();//Error: King must be instantiated using "new"

属性和方法

每个函数都有两个属性:length和prototype,其中length是函数定义的参数个数

function sayName(name)
{
console.log(name);
}

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

console.log(sayName.length);//1
console.log(sum.length);//2

函数有两个方法:apply()和call()。

  • apply(this, arguments): 第一个参数是函数调用时的上下文对象,也就是this所指向的值,第二个参数可以是Array,也可以是arguments对象
    function sum(num1, num2)
    {
    return num1 + num2;
    }

    function callsum(num1, num2)
    {
    return sum.apply(this, arguments);//传入arguments对象
    }
    console.log(callsum(10, 10));//20
  • call(this, arguments): 作用和apply相同,不同的是call中的参数必须一个个列出来,而不能传数组

apply和call主要是用来切换this对象用的,例如

window.color = 'red';
let o =
{
color: 'blue'
};

function sayColor()
{
console.log(this.color);
}

sayColor();

sayColor.call(this);//red
sayColor.call(window);//red
sayColor.call(o);//blue

  • bind(this): 创建一个新的函数实例,并且将实例内部的this与传入的this对象进行绑定。例如:
window.color = 'red';
var o =
{
color: 'blue'
};

function sayColor()
{
console.log(this.color);
}

let objectSayColor = sayColor.bind(o);
objectSayColor();//blue

递归

递归就是自己调用自己,一般由递归表达式和终止条件。js写递归时有它自己的特色

function factorial(num)
{
if(num <= 1)
{
return 1;
}
else
{
return num * arguments.callee(num - 1);//这样不会受函数名干扰
}
}

但是在严格模式下,不可以使用callee,可以替换成如下形式

const factorial = (function f(num)
{
if(num <= 1)
{
return 1;
}
else
{
return num * f(num - 1);
}
});

递归的一大缺点就是内存占用高,而js新增一项内存管理机制(尾调用优化)可以在条件满足的时候重用栈帧

例如:

function outer()
{
return inner();
}

原来它的调用过程是这样的

  1. 执行outer,第一个栈帧生成
  2. 发现返回inner,计算inner,并且第二个栈帧生成
  3. 计算完inner,第二个栈帧退出,然后返回给outer,第一个栈帧退出

现在是:

  1. 执行outer,第一个栈帧生成
  2. 发现inner,但是此时发现outer返回值和inner返回值相同,弹出outer栈帧
  3. 计算inner并返回

尾调用优化的条件:

  1. 在严格模式下执行
  2. 外部函数的返回值是对尾调用函数的调用(必须是在return中调用)
  3. 尾调用函数返回后不用执行额外的逻辑(如返回值加减等)
  4. 尾调用函数没有引用外部函数中变量

例如:

function outer()
{
let foo = 'bar';
function inner(){return foo;}
return inner();//闭包,没有优化
}

function outer()
{
return inner().toString();//执行了额外的逻辑
}

function outer(a, b)
{
if(a < b)
{
return a;
}
else
{
return inner(a + b);//有优化,前面一个return可以提前得知
}
}

闭包

闭包是一个函数引用了另一个函数中的变量,这通常是在嵌套函数中实现的。例如

function createComp(propertyName)
{
return function(object1, object2)
{
let value1 = object1[propertyName];//引用了外部变量
let value2 = object2[propertyName];
if(value1 < value2)
{
return -1;
}
else if(vlaue1 > value2)
{
return 1;
}
else
{
return 0;
}
};
}

前面已经说过作用域链
。在函数执行时,每个函数都有一个包含其变量的对象。在全局上下文中叫变量对象,在局部上下文叫活动对象,它会随着函数的销毁而销毁。

例如

function compare(value1, value2)
{
if(value1 < value2)
return -1;
else if(value1 > value2)
return 1;
else
return 0;
}
let result = compare(5, 10);

这个例子中在定义compare函数时,会为他创建作用域链,并且预装载全局变量对象,保存在Post not found: Scope中。在调用这个函数时,会复制Post not found: Scope来创建作用域,并且将自己的活动对象放在作用域链的前端。

而在createComp中,里面的匿名函数引用了createComp中的变量,因此当他返回之后createComp并不会销毁,只有在匿名函数引用清除之后才会被销毁

let compare = createComp('name');
let result = compare({name: 'a'}, {name: 'b'});

compare = null//清除引用,现在createComp才会被销毁

闭包中的this对象

window.identity = 'The Window';

let object =
{
identity: 'object',
getIdentityFunc()
{
return function()
{
return this.identity;
};
}
}

console.log(object.getIdentityFunc()());//The Window

结果返回按常理说应该是object,但是却是window。考虑对象的生成过程,其中有一步就是把this变量赋给函数,但是没有赋给函数的函数,也就是说闭包内部的this是原始window。如果我们想要让他的this是对象,可以

let object = 
{
identity: 'object',
getIdentityFunc()
{
let that = this;
return function()
{
return that.identity;
};
}
}

私有变量

例如:

function Person(name)
{
this.getName = function()
{
return name;
};

this.setName = function(value)
{
name = value;
};
}

let person = new Person('a');
console.log(person.getName());//a

上面的name实际上是一个私有变量,因为它的作用域是在函数内部的,可以通过参数名或arguments在函数中访问,但是函数外部无法访问,只有通过get和set才可以访问。

静态私有变量

上面那种是每个实例都有的私有变量,有时候需要所有实例共享私有变量,也就是静态私有变量

(function()
{
let name = '';

Person = function(value)//Person前面没有修饰符,所以自动将定义域归结到全局上下文
{
name = value;
};

Person.prototype.getName = function()
{
return name;
};

Person.prototype.setName = function(value)
{
name = value;
};
})();

let person = new Person('a');
console.log(person.getName());//a

通过这种方式可以让所有person实例修改一个name属性,由于Person是闭包,所以这个匿名函数调用之后并不会销毁。