[译] JavaScript 之 函数

“ 函数是 JavaScript 的精髓 , 同时也是这门语言的魅力所在 ” —— Douglas Crockford

原文地址:The power of JavaScript Functions

函数( Functions )可以说是 JavaScript 中最强大的对象,其他编程语言中很多不同的特性功能,比如:程序( procedures ),方法( methods ),构造函数( constructors ) 甚至是 类( classes )和 模块( modules ) 等 ;所有这些,在 javaScript 中都可以用函数实现

在最新版的 Javascript 中,引入了类( Class ) 、方法( method )和构造函数( constructor )等概念,其实这些只不过是用函数包装的语法糖

一旦你理解了函数的细节微妙,你其实就掌握了相当一部分 JavaScript ;当然,你还得学习在不同的作用域有效的使用函数,才能更上一层楼

本文的目的就是为你揭开函数的神秘面纱,让你不仅看到它的表象,更能洞彻它的精髓

函数对象(Functions as objects)

Javascript 的一个重要的特性:函数是一种特殊类型的对象。声明一个函数和声明一个对象其实没什么两样,因为声明一个函数就是产生一个函数对象

函数对象是第一类对象( first classed object ) : 它可以当作参数来传递,可以是其他函数返回的对象,可以把它赋值给一个变量,也可以存储在对象或者数组中。它继承自Function.prototype ,所以函数还可以直接调用 bind() , apply() 和 call() 方法

将函数看作是一个特殊类型的对象,对有其他编程语言经验的人员来说,可能是个不小的挑战;但是一旦你接受了这个设定,则对于理解JavaScript非常有帮助

函数结构(Function structure)

声明一个函数有如下两种方式:

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

function sum(a, b) {  //函数声明
    return a + b;
}

第 1 种方式,函数名是可选的(上面的示例就没有函数名);第 2 种方式,必须要有函数名

另一个值得注意的是,JavaScript没有”函数签名(function signature)”的概念,你可以传入零个或者多个参数去调用函数,而不用担心会报错;如果函数需要的参数你没传入,那么参数值默认就是undefined 。如果你想知道传入的参数是什么?你可以通过 arguments 对象来查看。示例如下:

function sum(a, b) {
    for (let i = 0; i < arguments.length; i++) {
        console.log("argument: " + arguments[i]);
    }
    console.log("sum: " + (a + b));
}

sum();
sum(1, 2, 3);

输出结果:

sum: NaN
argument: 1
argument: 2
argument: 3
sum: 3

函数没有明确要求一定得返回什么,如果一个函数没有明确返回的对象,那么默认返回 undefined. 谨记:

  • 函数签名概念在js中不存在 —— 函数可以被传入任意参数调用
  • 函数永远会返回一个值 —— 如果没明确提供返回的值,那么返回undefined(构造函数除外,它一定会返回新对象)

变量作用域(Variable scope)

变量作用域决定了变量的可访问性(可见性) , 在JavaScript中,有两种作用域—全局(global)和本地(local 主要是只函数内的 )

当一个函数被调用,那么一个作用域就形成了,本地变量只能在本地作用域内被访问

当使用var声明一个变量时,它会自动添加到最直接的可用范围中。在函数中,最直接的可用范围是函数的上下文环境。由于局部变量只能在函数内部识别,所以同名的变量可以用于不同的函数中

Javascript的一个特殊性是它没有块级范围。块级范围意味着如果一个变量在一个块内声明(for、while、if等),那么它只能在块内访问,不能在块外访问。例如,你可能期望这段代码能正常工作:

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
function print_matrix(matrix, n) {
    for (var i = 0; i < n; ++i) {
        row = matrix[i];
        for (var i = 0; i < n; ++i) {
            console.log(row[i]);
        }
    }
}

print_matrix(matrix, 3);

输出结果:1 2 3 问题出在哪儿呢? 因为第一个 for 的变量 i 每次都会在第二个for中重新赋值,这个函数将在第一行之后退出。 为了解决这个问题,在最新版本的Javascript (ECMAScript 6)中引入了关键字let。在块内部使用let声明的变量将具有块级范围。所以简单地改变var,上面的例子就能按照你的期望执行

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
function print_matrix(matrix, n) {
    for (let i = 0; i < n; ++i) {
        row = matrix[i];
        for (let i = 0; i < n; ++i) {
            console.log(row[i]);
        }
    }
}

print_matrix(matrix, 3);

变量提升(Variable hoisting)

提升是Javascript中的一种默认行为,它表示在代码执行之前将所有变量的声明移到其作用域的顶部。所以在函数中声明一个变量并不重要,变量从执行上下文开始就属于那个范围。但是,提升只针对变量声明,而不是变量赋值。所以在到达变量被显式赋值的那一行之前,变量的值是undefined

function myFunction() {
    console.log(myVar); // undefined
    var myVar = 3;
    console.log(myVar); // 3
}

myFunction();

因为变量提升,所以上面的代码相当于下面这段代码:

function myFunction() {
    var myVar;          // 变量定义会被提升到最顶端
    console.log(myVar); // 此时变量并未赋值,所以输出undefined
    myVar = 3;          // 变量赋值不会提升
    console.log(myVar); // 3
}

myFunction();

这适用于任何变量和函数表达式,但是,函数表达式和函数声明之间有一些细微的区别。提升会把声明移动到当前作用域最顶端,但是不包括赋值(初始化),在赋值(初始化)之前,变量的值是undefined

函数表达式 vs 函数声明

在继续之前,我有一件事要坦白。文章开头引人注目的引语是不完整的。我漏掉了一部分。让我们看看完整版:

“ 函数是 JavaScript 的精髓 , 同时也是这门语言的魅力所在。当然,像JavaScript中的其他东西一样,它很难确切的掌握 ” —— Douglas Crockford

就比如函数,就有两种声明函数的方法,它们之间还有细微的差别,这就是“不太确切”的一个例子

区别在于浏览器如何将它们加载到执行上下文中。函数表达式的加载方式与其他变量完全相同。当编译器到达这行代码时,它们被赋值(在本例中是函数对象)

但是,函数声明在执行任何代码之前加载。因此,即使函数在调用它的代码行下面声明,它也能工作。谨记:

  • 当你调用一个函数表达式,而它的声明代码在你调用的代码后面时,会报错
  • 当你调用一个函数,哪怕这个函数在你调用的代码后面声明,它依然能正确执行(因为它在编译之前会被提升到当前作用域前面)

为什么函数有这两种定义?

问的好,函数声明是在ECMAScript 3中引入的,它让我们记住Javascript是浏览器的语言,而用户很少更新浏览器。很多时候,浏览器的新版本只有在用户更新操作系统时才会接触到他们。所以为了考虑兼容性,就有了这两种定义方式

如前所述,从数据类型的角度来看,每个函数都是一个对象。函数对象有三个非常重要的属性来表示函数的身份: this 、arguments 和 prototype

this 是啥?

关于JavaScript的 this ,可以写一篇很长的文章,此处不打算详细介绍。只要记住this 是指向调用它的对象, 所以 this 的值在函数调用时分配

当从全局作用域简单地调用一个函数时,this 显然会指向全局作用域本身。这是造成混乱的一个重要原因,这就是为什么在严格模式下运行时,值是未定义的。通常,您不会对这样的函数使用 this , This 对于用作方法的函数非常有用。这样,该方法就可以访问对象属性

var myObj = {
    name: "John",
    sayName: function (name) {
        console.log("Hi, " + this.name); // 'this' 指向 myObj 
    }
};

myObj.sayName();

“This”在函数用作构造函数时也有重要作用。构造函数没有任何特殊的结构。当使用运算符new调用它时,它是一个构造函数。该操作符在底层创建一个新对象,并通过’ This ‘将其传递给函数

function Car(name) {
   this.name = name;
}

var myCar = new Car("Subaru");
console.log(myCar.name);

在上面的例子中,函数Car被用作一个函数。用大写字母声明它是一种常见的做法,目的很明显,但它只是一个简单的函数

执行new关键字,其实执行了如下步骤:

  • 创建一个空对象
  • this指向新创建的对象,给新对象增加name属性并赋值
  • 将Car函数的prototype赋值给新对象的prototype
  • 返回创建的对象

每个函数都可以访问从 Array.prototype 继承的.apply()、.bind()和call()。它们允许使用显式设置的’ this ‘的任何值来调用函数。一般来说有以下四种方式调用一个函数:

  • function form (‘this’ will be the global object or undefined in strict mode)
  • method form (‘this’ will reference the object that made the call)
  • constructor form (‘this’ will be the new object)
  • apply/bind/call form (‘this’ is explicitly set)

函数参数(Function arguments)

如前所述,声明中提供的参数不是可靠的信息来源。可以使用不同数量的参数调用该函数。如果它们太多,则忽略额外的参数。如果它们太少,丢失的值将是未定义的。

幸运的是,函数的arguments属性在这里可以帮助我们。它是一个类似数组的对象,包含调用时提供的参数。注意,它并不是一个真的数组,因为它不是从数组继承的,它唯一能调用的就是获取数组的长度,参数的存储方式与数组完全一样,索引从0开始

虽然arguments对象不是只读的,但是强烈建议不要修改它。更改arguments对象也会影响已命名的参数,这很容易引起混淆。

它的一个常见用法是创建具有未定义数量的参数的函数。例如,一个函数返回它的参数和:

function sum() {
    var i;
    var total = 0;
    var n = arguments.length;
    for (i = 0; i < n; i++) {
        total += arguments[i];
    }
    return total;
}
console.log(sum(1, 2, 3, 4)); // 10 

记住:参数对象是关于函数调用时提供的信息的惟一可靠来源,要将参数视为只读结构

函数原型(Function Prototype)

函数的原型对象只有在函数用作构造函数时才有意义。它在 Javascript 中创建继承时起着关键作用。通过它,使用相同构造函数创建的对象可以共享相同的属性和行为

原型继承是一个庞大的主题,超出了本文的范围,以后有时间再详谈

发表评论

电子邮件地址不会被公开。 必填项已用*标注