《JavaScript 模式》读书笔记(4)— 函数1

  从这篇开始,我们会用很长的章节来讨论函数,这个JavaScript中最重要,也是最基本的技能。本章中,我们会区分函数表达式与函数声明,并且还会学习到局部作用域和变量声明提升的工作原理。以及大量对API、代码初始化、程序性能等有帮助的模式。

  我们首先,要来回顾一些基础知识以明确一些概念和定义。

 

一、背景

  JavaScript中的函数有两个主要特点显使其显得比较特殊。第一个特点在于函数是第一类对象(first-class object),第二个特点在于它们可以提供作用域。函数就是对象:

  • 函数可以在运行时动态创建,还可以在程序执行过程中创建。
  • 函数可以分配给变量,可以将它们的引用复制到其他变量,可以被扩展,此外,除少数特殊情况,函数还可以被删除。
  • 可以作为参数传递给其他函数,并且还可以由其他函数返回。
  • 函数可以由自己的属性和方法。

  因此,对于函数A来说,他可能是一个对象,并且具有自己的属性和方法,而且其中的方法之一可能恰好又是另一个函数B。此外,函数B可以接受函数C作为参数,并且在执行时可以返回另外的函数D。

function A(){};
A.name = "aName";
function D(){console.log(1234)};
function C(){
    return D();
};
A.B = function (callback){
    callback()
};
A.B(C);

  代码看起来就像上面这样。乍看之下,有许多的函数要记录。但是适应各种函数应用后,将开始欣赏函数所提供的能力、灵活性、以及表现力。一般来说,当考虑JavaScript中的函数、对象时,其唯一的特性在于该对象(即函数)是可调用的,这意味着它是可执行的。

  事实上,当看到new Function()构造函数运行时,函数就是对象的意义就变得非常明确了:

// 反模式
// 仅用于演示目的
var add = new Function('a','b','return a + b');
add(1,2); // returns 3

  在以上这段代码中,毫无疑问,add()是一个对象,毕竟它是由一个构造函数所创建。然而,使用Function()构造函数并不是一个好主意,如同使用eval()一样不好。这是由于代码是以字符串方式传递并重新计算。同样,这也不便于编写和阅读,这是因为必须使用引号隔开代码,并且如果出于可读性的目的而希望在函数中正确地缩进代码,那么还需要格外注意。

  第二个重要的特征在于函数提供了作用域。在JavaScript中并没有块级作用域(当然,let出现之后,已经有了块级作用域,这里我们不讨论)。函数内部以var关键词定义的任何变量都是局部变量,对于函数外部是不可见的。考虑到花括号{}并不提供作用域(这句话是没问题的,哪怕是在现在的ES6出现之后,因为提供作用域的并不是花括号,而是花括号内使用let声明),因此如果在if条件语句或在for以及while循环中,使用var关键词定义一个变量,这并不意味着定义了一个局部变量。它仅对于包装函数来说是局部变量,并且如果没有包装函数,它将成为一个全局变量。

 

消除术语的歧义

  让我嗯话费一点时间讨论用于定义函数的相关代码的术语,因为在谈论到模式时,使用准确、约定的名称与代码是同等重要的。

// 命名函数表达式
var add = function add(a,b) {
    return a + b;
};

  上面的代码显示了一个函数,它使用了命名函数表达式(named function expression)。如果跳过函数表达式中的名称(例子中的第二个add),将会得到一个未命名函数表达式,也简称为函数表达式,或者最常见的是将之称为匿名函数。

// 函数表达式,又名匿名函数,未命名函数表达式
var add = function(a,b) {
    return a + b;
};

  因此,广义上称为函数表达式,并且命名函数表达式是一个函数表达式的一种特殊情况,通常发生在定义可选的命名时

  当省略了第二个add并且以一个未命名函数表达式作为结束,这并不会影响该函数的定义以及后续的调用。唯一的区别在于该函数对象的name属性将会变成一个空字符串。name属性是JavaScript语言的一个扩展(它并不是ECMA标准的一部分),但是在许多环境中得到了广泛的应用。如果保留了第二个add,那么add.name属性将会包含字符串“add”。当使用调试器时,或者当从自身递归调用同一个函数时,name属性时非常有用的,否则,可以跳过该属性。

  最后,获得了函数声明(function declaration)。这些声明看起来与其他语言中所使用的函数极为相似:

function foo() {
    // 此处为函数主体
}

  就语法而言,命名函数表达式与函数的声明看起来很相似,尤其是如果不将函数表达式的结果分配给变量(后面的回调模式中会看到)的时候。有时候,没有其他方法可以区分出函数声明和命名函数表达式的差异,除非查看函数出现的上、下文预警,正如将在下一节中所看到的。

  在尾随的分号中,这两者之间在语法上存在差异。函数声明中并不需要分号结尾,但在函数表达式中需要分号,并且应该总是使用分号,及时编辑其中分号自动插入机制可能帮您完成了这个工作。

  函数字面量,这个术语也经常被使用,它可能表示一个函数表达式或命名函数表达式。由于这种模糊性含义,并不推荐使用该术语。

 

声明vs表达式:名称和变量声明提升

  因此,应该使用哪种方法?函数声明还是函数表达式?在不能使用声明的情况下,下面将为您解决这种困境。

// 这是一个函数表达式
// 它作为参数传递给函数“callMe”
callMe(function (){
    // 这里,即该函数是一个匿名函数表达式
    // 也被称为匿名函数
});

// 这是一个命名函数表达式
callMe(function me() {
    // 这里,即me,是命名函数表达式
    // 名称是me
});

// 另一个函数表达式
var myobj = {
    say:function() {
        // 这里是函数表达式
    }
};

  上面的代码,展示了将函数对象作为参数传递,或者在对象字面量中定义方法。

  注意了:函数声明只能出现在“程序代码”中,这表示它仅能在其它函数体内部或全局空间中。它们的定义不能分配给变量或者属性,也不能以参数形式出现在函数调用中。

// 全局作用域
function foo() {}

function local() {
    // 局部作用域
    function bar() {}
    return bar;
}

  上面的代码,foo()、bar()、local()都是以函数声明模式进行定义的。

 

函数的命名属性

  当选择函数定义模式的时候,另一个需要考虑的事情是有关制度name属性的可用性。同样,这个属性并不是标准,但在许多环境中都可以使用它。在函数声明和命名函数表达式中,已经定义了name属性。在匿名函数表达式中,他依赖于其实现方式。其name可能是为定义的,也可能是空字符串来定义name属性。

function foo(){} //声明
var bar = function (){}; //表达式
var baz = function baz() {}; //命名表达式

console.log(foo.name); //输出“foo”
console.log(bar.name); //输出“bar”
console.log(baz.name); //输出“baz”

  注意,这里的一个区别,就是在现代浏览器中,若把一个匿名函数表达式赋值给一个变量,那么此时,匿名函数表达式的name属性即该变量的名字。因版本迭代原因,这与书中描述有些出入。

  name属性在调试bug和递归调用自身时很有用。其他场景可选择使用匿名函数表达式即可。

var foo = function bar() {};
console.log(foo.name)

  这样做也是可以的,打印出得结果是bar。这在技术上是没问题的,但是会存在一些兼容问题,所以不建议这样使用。

 

函数的提升

  从前面的讨论中,可能会得出函数声明的行为几乎等同于命名函数表达式的行为。然而这并不是完全正确,其区别在于提升(hoisting)行为。

  对于所有变量,无论在函数体的何处进行声明,都会在后台被提升到函数顶部。而这对于函数同样适用,其原因在于函数只是分配给变量的对象。“明白”的地方在于当使用函数声明时,函数定义也被提升,而不仅仅是函数声明被提升。

  

// 反模式
// 全局函数
function foo() {
    console.log("global foo");
}

function bar() {
    console.log("global bar");
}

function hoistMe() {
    // 在这里是为了判断提升的内容到底是什么,仅仅是变量名?还是连带函数体一起?
    console.log(typeof foo);
    console.log(typeof bar);

    // 执行
    foo();
    bar();

    // 函数声明
    // 变量“foo”以及其实现者被提升
    function foo() {
        console.log('local foo');
    }

    // 函数表达式
    // 仅变量‘bar’被提升
    // 函数实现未被提升
    var bar = function (){
        console.log('local bar');
    };
}
hoistMe();

  在这个例子中我们可以看到,如同正常的变量一样,仅存在与hoistMe()函数中的foo和bar移动到了顶部,从而覆盖了全局foo和bar函数。两者之间的区别在于局部foo()的定义被提升到顶部且能正常运行,即使在后面才定义它。bar()的定义并没有被提升,仅有他的声明被提升。这就是为什么代码执行到达bar()的定义时,其显示结果是undefined且并没有作为函数来调用(然而,在作用域链中,仍然防止全局bar()被“看到”)。

  最后强调一下函数的两个特征:它们都是对象,它们提供局部作用域。

 

  好了,这篇有关函数的基本情况和定义大家都了解了。下一篇我们继续。

posted @ 2020-03-26 15:11  Zaking  阅读(...)  评论(...编辑  收藏