Javascript 函数
函数声明可以采取命名函数、匿名函数、立即执行函数以及箭头函数。如下图代码段所示。命名在编程中一直就是一个难题,而一个好的函数命名往往是由一个动词开头然后接名词、副词等。一般如果函数是作为构造函数的话,则使用头字母大写,否则使用驼峰命名法。函数一般用来计算一个过程,然后返回一个结果,若没有显式返回,则返回undefined。
几种命名方式的区别:
- 使用function标识符加上函数名字来声明的函数会有变量提升作用,即在声明该函数前就可以调用了。而使用匿名函数来赋值给一个变量时(使用var声明),只有该变量声明被提前,而匿名函数的声明不会被提前。
- 箭头函数与function标识符声明函数的区别:在箭头函数中,this指针指向的是箭头函数定义生效时的父作用域,比如setTimeout(() => {someCode;});中的箭头函数的this指针指向的是setTimeout函数的作用域。而function标识符声明的函数的this指针指向的是调用该函数的对象。
函数调用的方式有三种:
- 作为函数:直接使用函数名,然后接括号传参。此时,函数内this指针在严格模式下指向undefined,非严格模式下指向全局对象。
- 作为方法:将函数赋值给某个对象的属性,然后使用对象的属性访问表达即可调用该函数。函数内this指向调用该函数的对象。
- 作为构造函数:使用new 标识符后面接上函数名以及括号内传参。构造函数中的this指向即将通过构造函数所新建的对象实例。实质上,构造函数执行的时候会先把创建一个对象,然后将构造函数设置为该对象的constructor方法,之后再通过该对象调用constructor方法来初始化对象的实例属性,之后设置该对象的原型为构造函数的原型。最后返回该对象。
Javascript中函数调用可以使用和形参数量不一致的实参。形参就是函数定义时所需要的参数。实参就是函数调用时所传入的参数。当实参与形参不匹配时,多则不赋值给形参,少则将多余的形参设置为undefined。
函数内部除了通过形参来获取参数以外,还可以通过arguments标识符来获取参数。arguments标识符是一个类数组的对象,有默认定义的遍历器,因此可以通过for..of来遍历,也可以使用拓展运算符...,但是没有定义数组的一系列方法。通过arguments标识符来获取多余的实参。因此可以定义不定形参的函数。
Tips: arguments对象还定义了默认的属性caller和callee,caller指代正在调用当前函数的函数,通过arguments.caller可以访问调用栈,callee则指代当前正在被调用的函数,通过arguments.callee可以在匿名函数中递归地调用自身。
Tips:当函数形参过多时(超过三个),最好传入一个对象,使用key-value的形式的将参数保存在该对象中。这样避免调用者还需要注意过多的实参之间的排序问题。
Tips:函数的原型是对象,这意味着可以给函数自定义一些属性,可以用来存储函数计算的中间结果等。
//声明函数的方式
function sqrtWithName(x) {
return x * x;
} //命名函数
let sqrt = function () {
return x * x;
};//匿名函数赋值给一个变量
(function (x) {
return x * x;
})(x);//匿名函数立即执行
let sqrtWithArrow = (x) => {
return x * x;
};//箭头函数
//函数调用
//作为函数调用
sqrt(10);
//作为方法调用
let myMath = {
sqrt: sqrtWithName
};
myMath.sqrt(10);
//作为构造函数调用
function Math(sqrtName) {
this.sqrt = sqrtName;
}
myMath = new Math(sqrtWithName);
//通过arguments获取参数
function sum () {
let sum = 0;
for (let arg of arguments) {
sum += arg;
}
return sum;
}
//匿名函数调用自身,获取一个50以内的斐波那契数列
let fibonacciArray = [];
(function (arr, n) {
if (n === 0) {
arr[0] = 0;
return arr[0];
}
if (n === 1) {
arr[1] = 1;
return arr[1];
}
let fib = arguments.callee;
arr[n] = fib(arr, n - 1) + fib(arr, n - 2);
return arr[n];
})(fibonacciArray , 50);
作用域链
作用域:一个变量的作用域是程序源代码中定义这个变量的区域。
执行环境(Execution Context):执行环境定义了变量以及函数有权限访问的其他数据,决定了他们各自的行为。每一个执行环境中都有一个变量对象(variable object)与之对应。这个变量对象中保存了该执行环境中的所有变量和函数。
词法作用域(Lexical Scoping):与块级作用域想对立。函数的执行依赖于变量作用域,作用域是在函数定义的时候就确定的,而不是在函数调用时决定的。Javascript中的词法作用域一大体现就是每个函数都有自己的执行环境,而for等代码块没有。
作用域链 (Scope chain):当代码在一个执行环境中执行时,会创建变量对象的一个作用域链。作用域链的作用是保证对执行环境中有权访问的所有变量和函数的有序访问。作用域链的前端都是当前所在执行环境的变量对象,之后是其父执行环境的变量对象,以此类推,直到全局执行环境。
标识符解析:标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止。
闭包
定义:函数对象可以通过作用域链关联起来,函数体内部的变量都可以保存到函数作用域中,这种特性叫做闭包。
用处:可以使用闭包捕捉到局部变量(参数),并且一直保存下来。看起来像是这些变量绑定到了其中定义他们的外部函数中。
使用闭包保存局部变量的标志:某个函数被链接到其作用域链所在的执行环境中,比如将函数返回给一个全局对象,当作该对象的一个方法。这样该函数的作用域链中的局部变量就可以被保存。
如下图代码段中用闭包解决setTimeout打印的问题。后面的代码之所以能正常工作,就是因为使用logAfterTimeout函数保存了当时执行环境中的变量对象中的局部变量 i,之后setTimeout中的函数可以通过作用域链获取到保存的局部变量 i。
闭包还有很多其他用法,比如保存私有变量等。
//有问题的代码
for (let i = 1; i <= 10; i++) {
setTimeout(() => {console.log(i);}, 100);
}//打印出来是10个10。
//使用闭包解决
for (let i = 1; i <= 10; i++) {
logAfterTimeout(i);
}//打印出来是1到10。
function logAfterTimeout(i) {
setTimeout(() => {console.log(i);}, 100);
}