前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JavaScript 模式》读书笔记(4)— 函数1

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

作者头像
zaking
发布2020-03-26 20:08:35
3890
发布2020-03-26 20:08:35
举报
文章被收录于专栏:zaking's

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

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

一、背景

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

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

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

代码语言:javascript
复制
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()构造函数运行时,函数就是对象的意义就变得非常明确了:

代码语言:javascript
复制
// 反模式
// 仅用于演示目的
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关键词定义一个变量,这并不意味着定义了一个局部变量。它仅对于包装函数来说是局部变量,并且如果没有包装函数,它将成为一个全局变量。

消除术语的歧义

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

代码语言:javascript
复制
// 命名函数表达式
var add = function add(a,b) {
    return a + b;
};

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

代码语言:javascript
复制
// 函数表达式,又名匿名函数,未命名函数表达式
var add = function(a,b) {
    return a + b;
};

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

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

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

代码语言:javascript
复制
function foo() {
    // 此处为函数主体
}

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

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

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

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

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

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

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

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

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

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

代码语言:javascript
复制
// 全局作用域
function foo() {}

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

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

函数的命名属性

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

代码语言:javascript
复制
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和递归调用自身时很有用。其他场景可选择使用匿名函数表达式即可。

代码语言:javascript
复制
var foo = function bar() {};
console.log(foo.name)

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

函数的提升

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

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

代码语言:javascript
复制
// 反模式
// 全局函数
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()被“看到”)。

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-03-26 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档