深刻解析js中的函数

写在前面

因为词语匮乏,本文继续沿用"深刻解析xxx"这个俗套的命名,可是是真的很深刻(你要信我啊)。若是本文对你有用,欢迎收藏,若是喜欢个人文章,欢迎点赞和关注专栏。
函数能够说是js的基础,无处不在,功能又十分强大,本文将简单介绍函数的特色而且重点介绍各类各样的用法。废话很少说,开车~
友情提示,因为本文涵盖的内容比较全面,难免篇幅稍长,中途请注意休息。css

函数简介

可是其实,函数的本质就是对象。确切一点来讲,实际上是第一类对象(first-class object)。关于第一类对象,wiki解释以下:java

第一类对象又称第一类公民,在编程语言中指的是一个具备如下特性的实体:node

  1. 可以做为参数被传递
  2. 可以从一个函数结果中返回
  3. 可以被修改和赋值给变量

虽然看起来高大上,可是咱们只要先记住,在js里函数也是对象,能够拥有本身的属性和方法,而它和通常js对象的区别是:能够被调用,也就是可执行web

固然,函数还有一个明显的特色就是,提供做用域:在函数做用域内的变量都是局部变量,对外部不可见。因为js中其余代码块,好比forwhile循环等并不提供做用域,因此有不少地方会利用函数来控制做用域。在后面会一一提到。编程

预备知识

这一块在以前讲闭包的时候其实提到了一些,可是仍是简单介绍下。segmentfault

函数做用域

在相似C语言的编程语言中,花括号{}表示一个做用域:在做用域内的变量对外不可见,这个称为块级做用域,可是在js中没有块级做用域,只有函数做用域:在函数体内声明的变量,在整个函数体内有定义api

function fun(){
    for(var j =1;j<10;j++){
        
    }
    console.log(j)//10
}
console.log(j)//undefined

这个例子中变量j定义在函数体中,那么在函数体内能够访问,在外部则没法访问。数组

做用域链

做用域链,就是一个相似链表的解构,它表示当前代码有权访问的做用域的访问顺序。举个例子:浏览器

var a = 1;
function fun(){
    var a = 2
    console.log(a)
}
fun()//2

在这里,执行fun()时,做用域链上有2个做用域,第一个是fun,第二个是全局环境,按照顺序,首先访问内容的做用域,找到了a变量,那么就不继续寻找,若是这里没有var a = 2,那么会继续向外寻找,最终输出的就是1缓存

只要记住,做用域链都是从当前函数做用域向外一层层延伸的,因此内部做用域能够访问外部变量,反之则不行。

声明提高

看下这个例子:

function fun(){
    console.log(a)
    var a = 1;
}
fun();//underfined

是否是以为很奇怪,这里既没有未定义报错,也没有输出1,由于这里的代码其实至关于这样写:

function fun(){
    var a;
    console.log(a)
    a = 1;
}
fun();//underfined

能够看到,其实变量a的声明,至关于被提早到当前函数做用域的顶部,这就是所谓的声明提高,可是要注意,声明虽然提高了,赋值a=1并无被提高,不然这个例子应该直接输出1

接下来再举1个例子回顾下这一阶段的知识:

var a = 1;
var b = 4;
function fun (){
    console.log(a);
    var a = 2;
    var b = 3;
    console.log(b);
}
fun ();
console.log(b);

具体结果你们能够跑跑看。

函数的建立

一般来讲,有2种建立函数的方式:函数表达式、函数声明。

函数表达式

函数表达式一般具备以下形式:

var funA = function funName(param1,param2){
    //函数体
}

固然,更常见来讲这里的funName是不写的,写与不写的区别是,在不一样浏览器中,得到的函数对象中name属性的值会被处理成不行的形式。

//这个例子能够在ie firefox webkit内核的浏览器分别跑一下看看结果 
var fun1 = function(){}
var fun2 = function funName(){}
console.log(fun1)
console.log(fun2)

写函数名字有个比较好用的地方是在递归的时候,能够很方便使用:

//阶乘函数
var fun1 = function recu(x){
    if(x<=1)
        return 1;
    else
        return x*recu(x-1)
}

函数声明

函数声明形式通常以下:

function funName(){
    //函数体
}

这个和函数表达式的区别就是,使用函数声明的方式在js里会有"提高",而使用表达式方式写没有提高因此函数表达式定义的函数没法提早使用

fun1();//fun1
fun2();//报错
function fun1 (){
    console.log("fun1")
}
var fun2 = function(){
     console.log("fun2")
}

由于前面说过,赋值部分不会提高,而函数表达式的写法本质上也是一个变量声明和赋值,形如var x = function...x的声明被提高,可是右边的赋值部分要等待代码执行到这句的时候才生效。

举个更容易理解的例子:

console.log(fun2)//underfined
fun2();//报错
var fun2 = function(){
     console.log("fun2")
}

同理,变量fun2已声明,但未赋值。因此这里console.log的时候不报错,运行的时候才报错。看不懂请再回顾下预备知识的声明提高部分。

函数参数

函数的参数通常分红形参和实参,形参是函数定义时预期传入的参数,实参是函数调用时实际传入参数。

参数数量不对等状况和arguments

Javascript没有在函数调用时对实参作任何检查。 因此可能出现如下状况:

  • 当传入的实参比形参个数要少的时候,剩下的形参会被自动设置为underfined,因此在写函数的时候,咱们常常要注意是否要给参数一些默认值

    function fun(a){
        var a = a || "" //若是传入a就使用a,不然a设置为空字符串
    }

    若是咱们的函数使用了可选参数,那么可选参数的位置必须放在最后,不然,使用者调用时候,就要显式传入underfind,好比fun(underfined,a)表示第一个参数不传入。

  • 当传入的实参比形参个数要多的时候,咱们能够经过标识符arguments对象来得到参数

    function fun(a){ if(arguments.length>1)console.log(arguments[1])};
        var a=1,b=2;
        fun(a,b);//2

    这个例子中,经过arguments输出了实参b的值。值得一提的是,arguments并非数组,而是一个对象,只是刚好使用数字为索引

calleecaller

es5的非严格模式下,咱们可使用calleecaller这两个属性,

  • callee 表示当前正在执行的函数,一般用法是在匿名函数中写递归调用
  • caller 表示调用当前正在执行函数的函数,能够用来访问调用栈,这个属性是非标准的,可是大部分的浏览器都实现。更详细的用法能够查看MDN。

函数的模式

模式其实就是函数的各类应用方式,也是本文的重点

api模式

api模式主要是给函数提供更好的接口。

回调模式

最前面已经提到,函数是对象,而且能够被做为参数传递给其余的函数。

当咱们把函数A传递给函数B,而且让B可以在某一时刻执行A,这种状况咱们称函数A是回调函数(callback function),简称回调。

举个例子,假设这样一个背景:假设如今咱们须要处理一批dom节点,处理大概分2步,第一步,筛选出符合要求的一部分节点,第二步,对这部分数据作一些css样式修改。那咱们通常会先想到这样写:

//筛选函数
function filterNodes(nodes){
    var  i = 0;
    var result = [];
    for(i = 0; i<nodes.length;i++){
        //根据条件筛选
        if(...){
            result.push()
        }
    }
    return result
}

//操做函数
function operte(nodes){
    var  i = 0;
    for(i = 0; i<node.length;i++){
        // 样式操做
        node[i].style...
    }
}

按照上面定义的2个函数,先用filterNodes筛选符合要求额节点,而后将结果做为operate函数的参数,这样逻辑上是彻底没问题的,只是有一个地方:其实咱们已经2次遍历了符合要求的节点:第一次是在筛选时,第二次是在样式操做时。这里有办法优化吗?,若是咱们直接把样式操做直接写到result.push()后面,是能够减小一次遍历的,可是这样filterNodes函数就不是一个纯粹的筛选节点的数了。因此咱们可使用回调模式来解决,只需稍微修改下:

//筛选函数
function filterNodes(nodes,callback){
    var  i = 0;
    var result = [];
    for(i = 0; i<nodes.length;i++){
        //根据条件筛选
        if(...){
            result.push()
            
            //在这里判断是否传递了样式操做函数,若是有,就执行样式操做
            if(callback){
                callback(nodes[i])
            }
        }
    }
    return result
}

function operte(node){
    //这里就没必要再次循环了
    // 样式操做
    node[i].style...
}

这样改造以后,2个函数依然各自拥有本身的逻辑,并且咱们能够经过调用filterNodes时,传递不一样参数的办法,来控制咱们想要的功能。

回调函数还有不少的常见用途:

  1. 异步事件监听
    最多见的例子莫过于咱们为文档添加监听事件:

    document.addEventListener("click",[回调函数],false)

    有了回调模式之后,程序能够以异步的模式运行:只有用户触发了某些交互行为,才会调用到咱们指定的函数。

  2. 超时方法 setTimeout()setTimeInterval()
    这两个函数也同样接受回调函数

    setTimeout([回调函数],200)
  3. 软件库设计
    设计一个库的时候,很重要的就是设计通用性和复用性的代码,由于没法提早预测到须要的每个功能,并且用户也不会老是须要用到全部的功能,利用回调模式,很容易设计出具备核心功能有同时提供自选项的函数(好比前面提到的节点筛选函数,核心功能是筛选,又能根据须要插入后续操做)。

返回函数

刚刚在回调函数部分,说的是函数做为另外一个函数的参数传递,接下来讲说函数做为另外一边函数的结果返回。看下面一个计时器例子:

var counter = function(){
    var count = 0;
    return function(){
        return count++
    }
}
var f = counter();
f();//1
f();//2

其实这里就是一个闭包的实例,关于闭包,在个人另外一篇文章里有更详细的描述点击前往

配置对象

配置对象模式其实就是让用对象做为函数的参数。
这种模式常常用在创建一个库,或者写的函数要提供给外部调用时。由于它能提供很简洁的接口。假设这样一个例子:

function operate(para1,para2){}

若是咱们正在写一个库函数,一开始咱们预料到的参数只会有para1,para2,可是随着不断拓展,后来参数变多了,并且出现了一些可选参数para3,para4:

function operate(para1,para2,para3,para4...)

此时咱们须要很当心的把可选参数放在后面,使用者在调用的时候还必须很当心的对上位置,好比说:

operate(p1,p2,null,p4)//这里的null不可省略

此时,参数数量太多,使用起来须要很当心记住参数顺序,很不方便。因此就要采用配置对象的写法,即把参数写成一个对象:

function operate(config){}
var conf = {
    para1:...,
    para2:...,
    para4:..., 
}
 operate(con)

这样的写法

  • 优势是:使用者不须要记住参数顺序,代码也显得更简洁,
  • 缺点是:使用时要严格记住参数的名称,而且属性名称没法被压缩

一般在操做dom对象的css样式时候会用这样的写法,由于css样式有不少,可是名称很容易记住,好比

var style ={
    color:"..."
    border:"..."
}

柯里化

start18/08/08编辑


柯里化内容已添加,传送门


end18/08/08编辑

柯里化的内容比较长,难度也稍大,后续另开一篇来写吧~~。

初始化模式

初始化模式的主要做用是不污染全局命名空间,使用临时变量来完成初始化任务,使任务更加简洁

即时函数

即时函数模式(immeddiate Function pattern),是一种支持在定义函数后当即执行该函数的语法。也叫做自调用和自执行函数
(function(){
    //函数内容
}())
//也能够这样写
(function(){
    //函数内容
})()

这里给出了即时函数的两种写法,它的做用是能够给初始化的代码提供一个存放的空间:好比在页面初始化时,须要一些临时变量来完成一次初始化,可是这些工做只须要执行一次,执行以后就再也不须要这些临时变量,那么咱们就没必要浪费全局变量来建立这些变量,此时使用即时函数,能够把全部代码打包起来,而且不会泄露到全局做用域。好比:

(function(){
    var initName = ""
    alert(initName)
}());

固然,即时函数也能够传递参数,

(function(initName){
    alert(initName)
}("hello"));

一样也能够有返回值:

var result = (function(){
  return 1
}());
console.log(result)//1

即时函数常常用在写一些自包含模块,这样的好处是能够确保页面在有无该模块的状况下都能良好运行,很方便的能够分离出来,用于测试或者实现,或者根据须要实现“禁用”功能。例如:

//moudle1.js
(function(){
    //模块代码
}//)

按照这一的形式写模块。能够根据须要加载模块。

即时对象初始化

这个模式和即便函数模式很类似,区别在于咱们的函数写在一个对象的方法上。一般咱们在一个对象上写上init方法,而且在建立对象以后当即执行该方法。以下:

({
    //初始化的属性和配置
    name:'Mike',
    age:'12',
    //其余方法
    ...
    //初始化
    init:function(){
        ...
    }
}).init();

这个语法其实至关于在建立一个普通的对象而且,而后在建立以后马上调用init方法。这种作法和即时函数的目的是一致的:在执行一次性初始化任务时保护全局命名空间。可是能够写出更加复杂的结构,好比私有方法等,而在即时函数里面只能把全部的方法都写成函数。

初始化时分支

初始化时分支常常用在某个生命周期中作一次性测试的情境中。所谓的一次性测试就是:在本次生命周期中,某些属性不可能改变,好比浏览器内核等。典型的例子是浏览器嗅探.

看过javacscript高级程序设计的话,对这个例子必定很眼熟:

var utils = {
        addListener:function(el,type,fn){
            if(typeof window.addEvenrtListener === 'function'){
                el.addEventerListener(type,fn,false);
            }
            else if(typeof window.attachEvent === 'function'){
                //ie
                el.attachEvent('on' + type,fn)
            }
            else{
                //其余浏览器
                 el.['on'+ type] = fn
            }
        }
        ...//删除方法相似
    }

这个例子是为了写一个可以支持跨浏览器处理事件的方法,可是有个缺点:每次在处理事件时都要检测一次浏览器的类型。咱们知道,其实在一次页面的生命周期里,其实只须要检测一次就够了,因此能够利用初始化分支来这样改写:

var utils = {
    addListener:null
}
if(typeof window.addEvenrtListener === 'function'){
    utils.addListener = function(el,type,fn){
        el.addEventerListener(type,fn,false);   
    }
}
else if(typeof window.attachEvent === 'function'){
    //ie
    utils.addListener = function(el,type,fn){
        el.attachEvent('on' + type,fn)
    }
}
else{
    //其余浏览器
     utils.addListener = function(el,type,fn){
        el.['on'+ type] = fn
     }
}

这样的话就能够在加载时完成一次嗅探。

性能模式

性能模式,主要是在某些状况下加快代码的运行。

备忘模式

备忘模式的核心是使用函数属性,缓存能计算结果。以便后续调用时能够没必要从新计算。
这么作的基础主要是以前提到过的,函数本质仍是对象(这句话已经重复n次了),既然是对象天然能够拥有属性和方法,例子:

var fun = function(key){
    if(!fun.cache[key]){
        //不存在对应缓存,那么计算
        var result = {}
        ...//计算过程
        fun.cache[key] = result
    }
    return fun.cache[key] 
}

这里举了一个比较简单的例子,在获取对应数据的时候,先判断有无缓存,有的话直接获取;没有的话计算一次并缓存到对应位置。以后便无需重复计算。

固然,这里的key咱们假设是基本类型的值,若是是复杂类型的值,须要先序列化。
另外,在函数内的fun能够经过前面提到的arguments.callee来代替,只要不在es5的严格模式下就行。

自定义模式

自定义函数的原理很简单:首先建立一个函数并保存到一个变量f。而后在建立一个新函数,也保存在这个变量f,那么f最终指向的应该是新的函数。那么若是咱们让这个过程发生在旧的函数内部,那么就实现了惰性函数。话很少说,看例子:

var fun = function(){
  console.log("在这里执行一些初始化工做")
  fun = function(){
       console.log("在这里执行正常工做时须要执行的工做")
  }
}
fun();//在这里执行一些初始化工做
fun();//在这里执行正常工做时须要执行的工做
fun();//在这里执行正常工做时须要执行的工做

在这里咱们执行了一次初始化任务之后,函数就变成了正常的函数,以后的执行就能够减小工做。

总结

这是2018年写的第一篇长文(其实一共就写了2篇,哈哈哈)但愿今年本身能够好好努力,把“深刻”系列贯彻到底。也但愿你们都有所进步。而后依然是每次都同样的结尾,若是内容有错误的地方欢迎指出;若是对你有帮助,欢迎点赞和收藏,转载请征得赞成后著明出处,若是有问题也欢迎私信交流,主页添加了邮箱地址~溜了

相关文章
相关标签/搜索