JavaScript中函数的前世今生(下)

129 阅读5分钟

前情提要:在上一篇文章中我主要介绍了在JavaScript中函数的基本概念以及一些基础知识,我们说到了函数的几种定义方式,函数的两个内部对象,函数参数的几种形式以及ES6箭头函数的出现带来的一些改变。在这篇文章中会继续介绍函数的基础知识,函数的尾调用,以及ES6箭头函数的特点。

私有变量

准确来说JavaScript是没有私有成员的概念的,所有的对象属性都是公有的,但是私有变量确不一样,我们知道每一个函数都会有它自己的作用域以及上下文,而定义在这个函数内的所有变量就是这个函数的私有变量,在这个函数外是无法访问该变量的。

我们知道如果在一个函数内部创建一个闭包,则这个闭包就可以通过其作用域链访问其外部的变量,基于这一点,我们就可以创建一个 “特权方法” 去访问函数的私有变量。

特权方法:是指能够访问函数私有变量或者私有函数的公有方法。在对象上创建特权方法有两种方式。

(1)构造函数中实现

//构造方法
function Myobject(){
//创建私有变量与私有函数
let gg=10;
function vv(){
return false;
 }
//创建特权方法
this.publicmethod=function(){//实际上这是个闭包
   gg++;
   return vv();
   }
}

(2)使用私有作用域定义私有变量与私有函数

(function (){
//创建私有变量与私有函数
 let bb=10;
 
 function aa(){
   return false;
 }
 //构造函数
 Myobject=function(){};
 //公有和特权方法
 Myobject.prototype.publicMethod=function(){
  bb++;
  return aa();
 };
})();

函数尾调用

尾调用:是指函数作为另一个函数的最后一条执行语句被调用。在ES6中实现了对尾调用系统引擎进一步优化。先来看一个例子感受一下:

function A(){
function B();//尾调用
}

这个例子非常简洁,我们来看一下ES6优化前后JavaScript是怎么执行的:

ES6之前

1.png

2.png

3.png

而在ES6之后

前面两步都是一样的,但是当执行到B的时候,JavaScript引擎发现此时即使将A弹出栈也没关系,因为在这里B的返回值实际上也是A的返回值。

故第四步就是弹出A,然后执行到B的时候将B入栈。后面就是将B的值返回。

显然ES6之后的执行模式,在函数调用栈中始终都是只有一个栈帧,而这就是ES6尾调用优化的关键:如果函数的逻辑允许基于尾调用将其销毁,那么引擎就会将其销毁。

我们知道在JavaScript的函数调用栈中,一个未用完的栈帧都会被保存在内存中,直到其不会再被使用才会将其销毁或者移除。尾调用的优化在递归中感触尤为深刻,试想如果要递归一个相同的函数非常多遍,其调用栈就会非常大,就会造成内存溢出的风险。

ES6缩减了严格模式下尾调用栈的大小(非严格模式下函数调用中会允许使用f.arguments和f.caller这种会引用外部函数栈帧的方法,也就不能使用优化了),只要满足以下条件,尾调用将不再创建新的栈帧而是清除并重用当前的栈帧,那么要满足尾调用函数优化的条件有哪些呢?

  • 外部函数的返回值是对尾调用函数的调用;
  • 尾调用函数返回之后不需要在执行额外的逻辑;
  • 尾调用函数不是引用外部函数作用域中自由变量的闭包。 下面来看几个对应的例子:
//无优化,函数没有返回
function A(){
   B();
}
//无优化,尾调用没有直接返回
function A(){
  let bb=B();
  return bb;
}
//无优化,尾调用是一个闭包
function A(){
    let foo='Joan';
       function B(){
          return foo;
       }
  return B();
}
//有优化,栈帧销毁前执行参数计算
function A(s1,s2){
return B(s1+s2);
}
//有优化,初始返回值不涉及栈帧
function A(s1,s2){
  if(s1<s2){
     return s1;
  }
return B(s1+s2);
}

函数模块化

函数的模块化也是现在前端编程最流行的写法,往往一个功能就是一个模块,所以函数的模块化是非常值的探讨的一个东西。目前我还没有对模块化进行系统性的总结,后期我会补上相关的知识内容,有兴趣的伙伴可以参考这篇文章:前端模块化:CommonJS,AMD,CMD,ES6 - 掘金 (juejin.cn)

name属性

由于在JavaScript中的函数有很多种定义方式,因此辨别函数就是一件具有挑战性的任务,加之匿名函数的广泛使用,更是加大了难度。因此在ES6中特意为所有函数都新增了name属性。

//声明一个函数
function welldone(){};
//声明一个函数表达式
var badfeel=function(){};
console.log(welldone.name);//'welldone'
console.log(badfeel.name);//'badfeel'

可以看到无论是一个函数还是一个函数表达式,调用name属性输出的都是该函数的名称。此外还有两个需要注意的地方就是通过bind()函数创建的函数,其名称将带有‘bound’前缀;通过Function构造函数创建的函数,其名称将是"anonymous".

var doSomething =function(){};
console.log(doSomething.bind().name);//"bound doSomething "
console.log(new Function().name);//"anonymous"

切记,函数name属性的值不一定引用同名变量,它只是协助调试用的额外信息,并不能使用name属性的值来获取函数的引用。

ES6的箭头函数

前面主要是对标准函数基础知识的介绍,接下来就要到我们现在最常用到的箭头函数了。

箭头函数是ES6新增特性,它和传统函数有些许的不同,但是我们知道一个领域中新东西的产生一般都是对原有事物的改良与优化,箭头函数就是如此。

相较于传统函数,箭头函数有以下的不同:

  • 没有this,super,arguments,new.target绑定,箭头函数中这四样东西都是由其外层最近的非箭头函数决定,也就是箭头函数的父级函数。
  • 不能通过new调用。前面我们说过箭头函数是没有[[Construct]]方法的,所以不能被用作构造函数,也就不能用new调用该函数。
  • 没有原型prototype,因为不能用new调用,所以也就没有构造原型的需求。
  • 不可改变this,箭头函数的this是不可改变的,要与其生命周期始终保持一致。
  • 不支持arguments对象,箭头函数是没有arguments对象的,所以必须通过命名参数或者不定参数访问函数参数。
  • 不支持重复命名参数,箭头函数无论是严格还是非严格模式下都是不支持重复命名参数的;而传统函数只有在严格模式下才不支持。

对比以上主要的差异可以看到吗,箭头函数的出现主要就是便利JavaScript引擎对函数进行优化。下面就来具体的介绍一下箭头函数的用法吧。

语法规则

箭头函数,顾名思义就是一个“胖箭头”,针对传入参数的不同主要有以下的几种表达方式:

//没有参数的时候
let result=()=>'Jaon';
//等同于
let result=function(){
   return 'Jaon';
};

//一个参数的时候
let result=value=>value;
//等同于
let result=function(value){
   return value;
};

//两个参数的时候
let result=(s1,s2)=>s1+s2;
//let result=(s1,s2)=>{
  return s1+s2;
}
//等同于
let result=function(s1,s2){
  return s1+s2;
};

//需要返回一个对象的时候
let result=id=>({id:id,name:'Joan'});
//等同于
let result=function(id){
    return {
    id:id,
    name:'Joan'
    };
};

显然,在大部分的情况下箭头函数都更加的简短,有助于我们精炼自己的代码。而且对于箭头函数其辨别方式除了形式上的,其他的方式和传统函数基本没有区别,同样可以使用instanceof , typeOf方法来检测。

var som=(a,b)=>a-b;
console.log( typeOf som);//'function'
console.log(som instanceof Function);//true

同样call/apply(),bind()方法也可以使用,只不过在箭头函数中调用这些方法是不会影响箭头函数的this。

var sum=(s1,s2)=>s1+s2;
console.log(sum(null,1,2));//3
//传入数组
console.log(sum(null,[1,2]));//3

//使用bind创建add函数
var add=sum.bind(null,1,2);
console.log(add());//3

此外,关于ES6中的箭头函数其他的特点在前面将传统函数的时候也有穿插这来讲,就不赘述了。可以查看我的上一篇文章:JavaScript中函数的前世今生(上) - 掘金 (juejin.cn)

最后,可算把JS函数部分大致理了一遍,这两篇文章花了我差不多四天的时间整理,如果有欠缺或者说的不是很准确的地方欢迎大家批评指正。