07|函数表达式

2019-12-03  本文已影响0人  井润

函数表达式是JavaScript中既强大又容易令人困惑的特性!

对应的定义函数的方式有两种:

  1. 函数声明

对应的函数声明是这样的,如下所示:

function functionName(arg0,arg1,arg2){
    //function body
}

其实通过看对应的代码其实就能很清楚的知道函数的具体含义或者功能是什么! function是关键字,之后就是对应的函数的函数名 在对应的FireFox,Safari,Chrome和Opera都给函数定义了一个非标准的name属性,具体的返回函数指定的名字(函数名)

console.log(functionName.name);//functionName

与此同时在函数声明中一个比较重要的特性就是:函数声明提升 对应的意思其实也容易理解:执行代码之前会先读取函数声明 这样一来函数生命可以放到函数调用语句之后的!

sayHello();//Hello Jay!
function sayHello(){
    console.log('Hello Jay!');
}

该例子不会报错是因为执行代码之前会先读取函数声明!

  1. 函数表达式

介绍完了对应的函数声明之后,我们就简要介绍一下函数表达式吧!函数表达式其实有多种写法,我们就是用最常见的函数表达式:

let functionName = function(arg0,arg1,arg2){
    //function body
}

看起来像是最常规的变量赋值语句,创建函数并且赋值给对应的变量! functionName 其实对应的function关键字后面没有对应的标识符,因此我们有两种叫法:

与此同时 他们的name属性都为空字符串!

还有就是,函数表达式并不会有函数提升!

sayHi();//Cannot access 'sayHi' before initialization
let sayHi = function(){
    console.log('Hello Julia!');
}

其实对应的控制台报的错其实很明白了:不要再sayHi初始化之前访问该函数

不要再对应的条件判断语句中使用 函数声明 !而是应该使用函数表达式!

if(condition){
    function sayHi(){
        console.log("TODO1");
    }
}else{
    function sayHi(){
        console.log("TODO2");
    }
}

不要在条件判断语句中使用函数声明是因为对应的函数声明提升的问题!

其实我们通过对应的代码含义上来看很清楚:

如果对应的condition为true使用第一个函数声明的定义,否则则使用另外一个!

但是实际上在ECMAScript中属于无效语法! JavaScript尝试修正错误,转换为合理的状态! 但是更为关键的是:

每个浏览器的处理的方式不同:

如果使用函数表达式会怎么样呢?

let sayHi ;
if(condition){
    sayHi = function(){
        console.log("TODO1");
    }   
}else{
    sayHi = function(){
        console.log("TODO2");
    }
}

01|递归

递归函数其实就是函数通过名字调用自身的情况导致的,如下代码所示:

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

以上的函数没有什么问题,但是如果说如下操作的话就会出错了:

let anotherFactorial = factorial;
factorial = null;
console.log(anotherFactorial(4));//Uncaught TypeError: factorial is not a function

其实这段代码我们可以这样理解:

要解决这个问题,就需要借助 arguments.callee() 函数,该函数主要的功能就是 一个指向正在执行函数的指针!

function factorial(num){
    if(num <= 1){
       return 1;
    }else{
        return num * arguments.callee(num -1);
    }
}

因此我们在编写对应的递归函数的时候,使用arguments.callee总比使用函数名更加的保险!

但是对应的arguments.callee不能够应用在严格模式下,但是我们可以使用函数表达式来解决该问题!

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

创建了一个名为f的命名函数/具名函数的函数表达式,将它赋值给变量factorial,即便把函数赋值给了另外一个变量,但是函数名f依然生效! 对应的递归调用依然能够正确完成!

02|闭包

其实在开发中很多开发人员容易把匿名函数和闭包搞混,但是闭包的概念是这样的:
有权访问另一个函数作用域中的变量的函数!
对应的闭包的创建方式最常见的方式是:在一个函数内部创建另外一个函数之前就讲到过作用域链,这对于闭包的理解至关重要!当某一个函数第一次被调用的时候,会创建一个执行环境及对应的作用域链,把作用域链赋值给一个特殊的内部属性,然后使用this,arguments以及其他命名参数的值来初始化函数的活动对象,函数对象按照层级划分,当前的活动对象为第一位,向外依次排列!外部的函数作为第二层,直至作为作用域链终点的全局执行环境!

对应的在函数的执行过程中,为了更加方便的读取和写入的值,就需要再作用域链中查找变量,如下所示:

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

其实通过以上的代码我们来说说这背后发生了什么?

  1. 以上代码先定义了compare函数,并且在全局作用域中调用了它!
  2. 第一次调用compare的时候,会先创建一个包含 this,arguments,value,value的活动对象!
  3. 全局执行环境下面的变量对象 (this,result,compare)在compare执行环境的作用域链中处于第二位!
  4. 每个函数中都存在一个表示变量的对象, 全局环境中的变量对象始终存在! 对应的compare这样的局部环境的变量对象,只在函数执行的过程中存在!

那么对应的当我们调用compare的时候发什么什么?

对应的闭包的情况有所不同,也就是

既然闭包是为了更好的帮助开发者进行开发,对应的缺点在哪里?

  1. 缺点就是因为闭包会携带包含他的函数的作用域! 会比其他函数占用更多的内存!
  2. 我们应该只在绝对必要的时候考虑使用闭包!尽管对应的V8引擎进行了优化,但是还是需要谨慎使用闭包!
01|闭包与变量

其实虽然说闭包是为了开发人员更好的进行开发,但是因为作用域的这种配置机制引出了一个值得我们注意的副作用,闭包只能够取得包含函数中任何变量的最后的一个值!是最后的一个值!
我们通过简单的代码示例表明:

function createFunctions(){
    let result = new Array();
    for(let i=0;i<10;i++){
        result[i] = function(){
            return i;
        }
    }
    return result;
} 

你猜一下对应的返回的数组中的每一项对应的值为多少?

可能会觉得每一项的值为0~9其实是不是的! 每一项的值都为10

我们可以通过以下的例子来说明:

function createFunctions(){
    let result = new Array();
    for(let i=0;i<10;i++){
        result[i] = function(){return i;}
    }
    return result;
}
console.log(createFunctions().map(currentItem=>curremtItem()));
/*
[
  10, 10, 10, 10, 10,
  10, 10, 10, 10, 10
]
*/

为什么会是这样的呢?

我们先通过调用该方法返回对应的数组之后,通过对应的map方法对数组中的每一项进行调用返回对应的i将每一项的返回值组成一个数组并且将属猪打印出来就像现在这样了!

闭包的特性导致了只能够取得包含函数中的变量的最后一个值,因此我们拿到的最后一个值i,便是0了!

那么有什么方法解决只能够拿到一个值的问题吗? 我们可以通过创建另外一个匿名函数让对应的结果符合预期!

function createFunctions(){
    let result = new Array();
    for(let i=0;i<10;i++){
        result[i] = function(num){
            return function(){
                return num;
            }
        }(i)
    }
    return result;
}
console.log(createFunctions().map(currentItem=>currentItem()));
/*
[
  0, 1, 2, 3, 4,
  5, 6, 7, 8, 9
]
*/

对应的我们修改之后的结果就是,在循环里面加了一个匿名函数,并且是以立即执行函数的形式将对应的返回值赋值给result数组!

02|关于this对象

对应的在闭包的使用过程中,this对象是根据函数的执行环境绑定的!

var name = 'The Window';
var object = {
    name:'My object',
    getNameFunction:function(){
        return function(){return this.name;}
    }
}
console.log(object.getNameFunction()());//The Window

那么对应的结果为什么会直接访问到外部的作用与中的name呢?

object.getNameFunction()()的调用就等于调用他返回的函数! 匿名函数中的this.name作为闭包的存在,可以直接访问到全局对象中的name!

有什么办法能够访问到对象自身的name属性值吗?

var name = "The Window";
var object = {
    name:"my Object",
    getNameFunction(){
        var that = this;
        return function(){return that.name};
    }
}
console.log(object.getNameFunction()());//my object

所以会是这样的结果是因为,在getNameFunctions函数中将当前的this赋值给了that表示该对象,而返回的函数中的this表示window,但是该函数返回的值是that.name等于object.name!因此拿到的值为 my Object

对应的this的值也可能会因为一些特殊情况而改变,如下所示:

var name = "The Window";
var Object = {
    name:"My object",
    getName(){return this.name;}
}
Object.getName()//My object
(Object.getName())();//My object
(Object.getName = Object.getName)();//The Window

第三条是因为先执行赋值语句之后在调用,this的值不能够保持因此拿到的是全局对象下的name!

03|内存泄漏

像之前第四章讲到的那样,但是如果说闭包中保存着一个HTML元素的话,那么意味着该元素将无法被回收,我们可以通过最简单的例子来说明:

function assignHandler(){
    let element = document.getElementById('example');
    element.onclick = function(){
        console.log(element.id);
    }
}

其实对应的element元素事件处理器的闭包,但是该闭包创建了一个循环引用,匿名函数保存对assignHandle活动对象的引用,导致无法减少element的引用数量! 只要对应的匿名函数存在的话,element的引用数量至少为1!但是我们还是可以手动进行处理的!

function assignHandle(){
    let element = document.getElementById();
    let id = element.id;
    element.onclick = function(){
        console.log(id);
    }
    element = null;
}

那么对应的以上代码,通过将element.id副本保存在一个变量中,闭包中引用该变量消除了循环引用! 但是如果说回到内存泄漏问题的解决上来说的话,其实没有什么变化!

闭包回音用饱含函数的整个活动对象,其中包含了element,对应的 即使没有直接引用element,但是对应的活动对象中还是会存在一个引用!

但是我们将element设置为null的话,就能够在一定程度上减少引用的数,能够解除对DOM对象的引用! 确保正常内存的回收!

03|模仿块级作用域

其实在JavaScript终是没有所谓的快就作用域的概念的,意味着在块语句中定义的变量实际上是在函数中而非语句中创建的!

通过代码来演示:

function outputNumbers(count){
    for(var i=0;i<count;i++){
        console.log(i);
    }
    console.log(i);//计数
}

对应的如果说在Java中的话,i只会在for循环中有定义,出了对应的循环之后就会被销毁! 但是在JavaScript中的话,i是被定义在函数中/活动对象中的,就算是错误的重新声明一个变量也不会改变它的值!

function outputNumbers(count){
    for(var i=0;i<count;i++){
        console.log(i);
    }
    var i;
    console.log(i);
}

JavaScript在这种情况下,只会对后续的声明视而不见,但是仍然会通过后续声明的变量初始化! 匿名函数可以用来模仿块级作用域来避免这个问题!

(function(){
    //这里是块级作用域!
})()

以上的代码表示一个 函数表达式! 如果说不好理解的话,我们可以将函数表达式写成下面这个样子:

let someFunction = funciton(){
    //这里是块级作用域!
}
someFunction();
function outputNumbers(count){
    (function(){
        for(var i=0;i<count;i++){
            console.log(i);
        }
    })();
    console.log(i);//Error
}

之所以会报错,是因为在for循环中插入了一个块级作用域,对应的for循环执行完毕之后对应的变量i就会被销毁了,对应的循环之外的i也就访问不到了!

对应的在很多大型项目的里面,有很多开发人员组成的项目为了避免全局变量和函数容易命名冲突,就可以使用 块级作用域来解决这个问题! 而不必搞乱全局作用域!

04|私有变量

在JavaScript中的私有变量是怎么一回事呢? 任何在函数中定义的变量都可以认为是私有变量,函数外部是不能够访问导函数内部的变量的,对应的私有变量包括:

既然之前说到了,函数外部是不能够访问到函数内部的变量的,但是如果说能够创建闭包的话,那么闭包的作用链可以访问到这些变量! 就可以使用闭包的特性创建用于访问私有变量的公有方法!

function Person(name){
    this.getName = function(){
        return name;
    };
    this.setName = function(value){
        name = value;
    };
}
let p = new Person("Julia");
p.setName('Jerry');
console.log(p.getName());//Jerry

像上面的方法一样,通过闭包的方式隐匿具体变量的获取和修改的细节! 但是想想面讲到的使用构造函数模式的缺点就是每一个实例都会创建同一组新的方法,而使用静态私有变量可以实现特权方法!

01|静态私有变量
(function(){
    var name = "";
    Person = function(value){name = value;};
    Person.prototype.getName = function(){return name;};
    Person.prototype.setName = function(value){name = value;};
})();
let p1 = new Person("Julia");
let p2 = new Person("Lisa");
p1.setName('Jerry');
console.log(p1.getName());//Jerry
console.log(p2.getName());//Jerry

因为对应的name变成了静态的所有实例共享的属性,因此在一个实例上面调用setName的话其他所有实例的name属性都会改变!

该方法虽然说对应的私有变量因为原型而增进代码的复用,但是每个实例都没有自己的私有变量!

与之对应的,多查找作用域链的一个层次,对应的会影响查找的效率! 这正是使用闭包和私有变量的一个鲜明不足之处!

02|模块模式

对应的模块模式是为了单例创建私有变量和特权方法! JavaScript是以对象字面量的方式创建单例对象的!

看代码:

let application = function () {
    // 私有变量和函数 
    let components = new Array();

    // 初始化 
    components.push(new BaseComponent());

    // 公共 
    return {
        getComponentCount() {
            return components.length;
        }, registerComponent(component) {
            if (typeof component === 'object') {
                components.push(component);
            }
        }
    }
}();

每个创建出来的单例都是Object的实例,因为还是需要使用对象字面量的形式进行表示!

03|增强的模块模式
let application = function () {
    // 私有变量和函数 
    let components = new Array();

    // 初始化 
    components.push(new BaseComponent());

    // 创建一个application的一个局部副本
    let app = new BaseComponent();

    // 公共接口
    app.getComponentCount = function () { return components.length; }

    app.registerComponent = function (component) {
        if (typeof component === 'object') {
            components.push(component);
        }
    }

    // 返回该副本
    return app;
}();

不同之处是app是BaseComponent的实例,app十里添加了能够访问的私有变量的公有方法! 最后是返回app对象!

其实对应的本章节,最主要的是关于闭包的使用和理解是非常关键的,与此同时要理清楚块级作用域和私有变量之间的关系!

上一篇 下一篇

猜你喜欢

热点阅读