0%
3245 字 --- 耗时5 分钟.

JS 进阶之路 : 深入理解函数的概念

之前一直觉得自己对 JavaScript 这门语言掌握的还可以,记得当初学这门语言的时候,也都是按照 Java 的思维去学的,以至于很多概念都没有弄明白。

最近在 codewars 做练习的时候,才发现,原理的东西,一个都没有搞懂,JS 最精华的函数、闭包都是在以自己的思维去揣测,而非真正的理解。之前看 《JavaScript 权威指南》,书中内容虽然说的很细,却没有说懂。

最近一周看了一下网上比较火的《JavaScript 忍者秘籍》,闭包、函数说的很清楚,不由感叹,真是一本进阶的好书呀。这本书帮我解决了很多困惑的问题,让我对 JS 这门语言有了深刻的认识,这一周,让我体会到了读书的愉悦,感觉学到了非常多的知识。

看书的时候,记了笔记,而我又想写点什么来记录下自己的成果,决定分几篇小博客来记录一下我的心得体会。

JavaScript 中的函数

JavaScript 中的函数形式多样,可以定义,也可以内联,这样子定义函数才有意思。

Java 是非函数式语言,而 JavaScript 中的函数式特性允许我们创建一个独立的实体函数,并将这个实体作为参数一样使用(这跟Python很像,不知道是谁先创的)。当别的函数或实体接收这个函数参数之后,就可以当作函数拿来用。

我在使用函数的时候,总结出这么一个特点,就是为什么有时候用 function(),而有时候用 function,(区别在于有没有括号)比如:

function sayHello(){  
    //do something
    console.log('hello');
}
//作为函数直接调用
sayHello()  // 'hello'  
//作为函数参数传递
var say = sayHello;  
console.log(typeof say === 'function') //true  
say() // 'hello'  

如果一个函数参数后面加了(),它就会立即执行,比如 sayHello(),就会跑到函数所在的位置把函数跑一边,这跟其他语言的函数调用一样;不加()会把函数作为参数传递。var say = sayHello,这个时候,say 变量也是函数变量,可通过 say()调用。

函数的作用域

关于函数内变量的作用域,必须要注意,否则变量会 undefined 的:

  1. 函数内部的变量的作用域开始与声明的地方,结束于函数的结尾,与代码嵌套无关;
  2. 命名函数的作用域是指申明该函数所在区域的整个函数范围,与代码嵌套无关;
  3. 整个 window 是一个包含所有代码的超大型函数。

第一个很好理解,在没有申明之前,变量无法使用,结束后也无法使用:

function theTest(){  
    console.log(test); //undefined
    var test = 'hello';
    console.log(test); // 'hello'
}
theTest();  
console.log(test); //error,  

第二个的意思大致是函数定义不像变量,不用考虑顺序,但要考虑作用域,及申明函数的整个区域:

sayHello(); // 'hello'  
function sayHello(){  
    console.log('hello');
    function inner(){
        console.log('inner');
    }
}
inner(); // error not defined  

关于第三个,window 就是一个全局变量,可以通过 this 来调用。this 算是 JS 中用的最妙的,this 是动态的,我们写了一个函数,而调用这个函数的主体是可变的,这和 Java 的继承很像。

关于作用域,有一点需要注意,看下面的代码:

function outer(){  
    console.log(text); // undefined
    var text = 'Hello';
    this.sayHello = function inner(){
        console.log(text);
    }
    sayHello(); // 'hello'
}
outer();  
this.sayHello() // 'hello'  

结果居然是 undefined => Hello => Hello,undefined 很好理解,text定义之前是无法调用的,但是在函数外面通过 this.sayHello()却可以访问已经失效的变量,这说明函数(这实际上是一个闭包,之后的博客会提到)一旦建立之后,其内部的变量会被保存,以便访问的时候不会出错。

this

面试官往往喜欢问一下关于函数中 this 的指代,实际上函数在调用的过程中会默认传递两个隐式参数,分别是 arguments 和 this(这两个都是带有传奇色彩的参数)。arguments 是一个伪数组,而this 的指代大概有下面几种:

  1. 作为函数调用,this 指代是 window;
  2. 对象的方法,this 指代改对象;
  3. new 构造器,经过一系列转换,this 最终的结果会赋值给创建 new 的那个变量,有个先后顺序,下面会有例子;
  4. call 、 apply 、bind 等函数通过参数指定 this 的值。

下面我自己写了一个例子,这是我在调试时候遇到的,可以总结第一条和第四条:

function test(){  
    var fn = this; //通过 call 方法 this 指代 myObj
    console.log(fn.name); // 'song'
    return function(note){
        console.log(note); // 'this is note'
        console.log(this === window); // true
        console.log(fn === myObj); // true
    }
}
var myObj = {  
    name : 'song'
};
test.call(myObj)('this is note');  

结果很明显,代码的执行顺序大致是:myObj => test.call(myObj) => test()(this是 call 来的,符合第四个规则) => myObj("this is note") => function(note)(this符合第一个规则)。

看下面分析,自己可以亲手调试一下:

第二条对象的方法指代对象,其实对象的这个方法是个函数,举个例子就好理解:

// 建立一个对象
var myObj={  
    name : 'song',
    func : function(){
        console.log(this.name); // 'song'
        console.log(this === myObj); // true
    },
} 
myObj.func();  

对第三条的解释,先看下面的函数:

function test(){  
    console.log(this === myObj) // false
    this.name = 'song';
}
var myObj = new test();  
console.log(myObj.name); // 'song'  

之所以 this === myObj会为 false,是因为new test()方法中的 this 并不是指向 myObj,它先是自己建立一个对象,把 this 指向自己,最终函数结束时候把 this 传递给myObj对象,console.log(myObj.name)才会有结果。

并不是我们所想的 this 指向 myObj。总之,这里的 this 不是指向 window。

cache 缓存,函数对象

如果说明函数是参数,可以传递,已经让人很兴奋,但事实上,函数还是对象,它是一个真实存在的变量。

下面是一个递归的 fibonacci 数列:

function fibonacci(n) {  
    if(n==0 || n == 1)
        return n;
    return fibonacci(n-1) + fibonacci(n-2);
}

(不讨论迭代,只考虑递归)我们知道递归会占用栈资源,当调用 1000000 次,可能会造成栈溢出。下面是一个采用函数内存的方法实现的 fibonacci 数列:

function fibonacci(n){  
    if(!fibonacci.cache) //在这里先判断fibonacci.cache是否存在
        fibonacci.cache=[0,1] //默认设置 0,1
    if(fibonacci.cache[n] != undefined)
        return fibonacci.cache[n];
    else{
        if(fibonacci.cache[n-1] === undefined)
            fibonacci.cache[n-1] = fibonacci(n-1);
        if(fibonacci.cache[n-2] === undefined)
            fibonacci.cache[n-2] = fibonacci(n-2);
        return fibonacci.cache[n] = fibonacci.cache[n-1] + fibonacci.cache[n-2];
    }
}

这种函数即对象的思路很有趣,当我们通过 fibonacci(10)运行 10 次之后,可以看到:

fibonacci(10);  
console.log(fibonacci.cache) //[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]  
console.log(fibonacci.name) // 'fibonacci'  

其实除了给函数对象添加的属性之外,它自己也有自己的属性,比如 name ,arguments , length 等。

函数的内容就写到这,共勉。