JavaScript权威指南(个人笔记):(四)函数

如果函数挂载在一个对象上,作为对象的一个属性,就称它为对象的方法。当通过这个对象来调用函数时,该对象就是本次调用的上下文(context),也就是该函数的this的值。

函数即对象。比如:可以把函数赋值给变量,或者作为参数传递给其他函数。因为函数就是对象,所以可以给它们设置属性,甚至调用它们的方法。

JavaScript函数可以嵌套在其他函数中定义,这样他们就可以访问他们被定义时所处的作用域中的任何变量。这意味着JavaScript函数构成一个闭包(closure)

(function() { }());  // 结束函数定义并立即调用他。注意:function之前的左圆括号是必须的,因为如果不写这个左圆括号,JavaScript解释器会试图将关键字function解析为函数声明语句。使用圆括号JavaScript解释器才会正确的将其解析为函数定义表达式。

函数的名称通常是动词!

如果函数里没有return语句,那么他就只执行函数体中的每条语句,并返回undefined给调用者。

 

函数的定义 

两种方式:

1.函数声明语句。

一条函数声明语句,实际上是声明了一个变量,并把一个函数对象赋值给他。

例子:funciton foo() { }  // 函数名foo是一个变量,变量指向函数对象

函数声明语句“被提前”到外部脚本或外部函数作用域的顶部, 所以可以被在他定义之前出现的代码所调用。

 

2.函数定义表达式。(可称为“函数直接量”)

注意:以表达式方式定义的函数,函数名称是可选的。定义函数表达式并没有声明一个变量。

函数可以命名,它需要一个名称来指代自己。如果一个函数定义表达式包含名称,函数的局部作用域将会包含一个绑定到函数对象的名称。实际上,函数的名称将成为函数内部的一个局部变量。

通常而言,以表达式方式定义函数时都不需要名称,这会让定义他们的代码更为紧凑。函数定义表达式特别适合用来定义那些只会用到一次的函数(比如下面展示的最后两个例子)

注意,以表达式来定义函数只适用于它作为一个大的表达式的一部分,比如在赋值和调用过程中定义函数,例子:

var square = function(x) { return x*x; }  // 将表达式赋值给一个变量

var f = function fact(x) { if (x <= 1) retrun 1; else return x*fact(x-1); };  // 函数表达式可以包括名称,这在递归时很有用(虽然可以用变量调用函数,但函数的名称还是fact

data.sort(function(a, b) { return a-b; });  // 函数表达式也可以作为参数传给其他函数

var tensquared = (function(x) {return x*x;}(10));  // 函数表达式有时定义后立即调用

注意:给变量赋值是不会提前的,因此,以表达式方式定义的函数在定义之前无法调用!

 

两种定义方式的区别:(只有函数名称区别,其他基本是没有区别了)

函数名称是函数声明语句必须的部分。它的用途就像变量的名字,新定义的函数对象会赋值给这个变量。

对函数定义表达式来说,这个名字是可选:如果存在,该名字只存在于函数体中,并指代该函数对象本身。

 

函数调用

4种方法:

1.作为函数

非严格模式下,调用上下文(this)是全局变量。严格模式下,调用上下文则是undefined。

以函数调用通常不用this关键字。不过,“this”可以用来判断当前是否是严格模式。

 

2.作为方法

如果有一个函数f和一个对象o,则可以用下面的代码给对象o定义一个名为m()的方法:

o.m = f;

给对象o定义了方法m(),如果需要两个参数,调用它时就像这样:

o.m(x, y);

上面的代码是一个调用表达式:它包括一个函数表达式o.m,以及两个实参表达式x和y,函数表达式本身就是一个属性访问表达式,这意味着该函数被当作一个方法,而不是一个普通函数来调用。

方法调用和函数调用有一个重要的区别,即:调用上下文(this)

属性访问表达式由两部分组成:一个对象(本例的对象o),和属性名称(m)。像这样,对象o即是调用上下文,可以用this引用该对象。

 

方法和this关键字是面向对象编程范例的核心。任何函数只要作为方法调用实际上都会传入一个隐式的实参,这个实参是一个对象,方法调用的母体就是这个对象。

方法链:当方法返回值是一个对象,这个对象还可以再调用它的方法。(当方法不需要返回值时,最好直接返回this)

需要注意的是,this是一个关键字,不是变量,也不是属性名。JavaScript不允许给this赋值。

很多人误以为调用嵌套函数时this指向调用外层函数的上下文。如果你想访问这个外部函数的this值,需要将this的值保存在一个变量里。通常用变量self来保存this。

 

3.作为构造函数

构造函数调用创建一个新的空对象,这个对象继承自构造函数的prototype属性

构造函数试图初始化这个新创建的对象,并将这个对象用做其调用上下文,因此构造函数可以使用this关键字来引用这个新创建的对象。

注意:尽管构造函数看起来像一个方法调用,它依然会使用这个新对象作为调用上下文。也就是说:

借用上面的例子,在表达式new o.m()中,调用上下文并不是对象o

 

4.间接调用

JavaScript中的函数也是对象,函数对象也可以包含方法。其中的两个方法call()和apply()可以用来间接的调用函数。

两个方法都允许显式指定调用所需的this值,也就是说,任何函数可以作为任何对象的方法来调用,哪怕这个函数不是那个对象的方法。

两个方法都可以指定调用的实参。

call()方法使用它自有的实参列表作为函数的实参。

apply()方法则要求以数组的形式传入参数。

 

可选形参

当传入的实参比形参少时,剩下的形参都将是undefine值。因此在调用函数时形参是否可选,是否可以省略,都应当保持较好的适应性。为了做到这一点,应当给省略的参数赋一个合理的默认值

例子:

function foo() {

  if(a)

}

 

a = a || [ ]  如果a是真值的话就返回a,否则返回空对象。这是一个习惯的用法。

 

可变长的实参列表:实参对象arguments

arguments[]数组只定义在函数体中

当调用函数的时候,传入的实参个数超过函数定义时的形参个数时,没有办法直接获得未命名值的引用。实参对象解决了这个问题。

在函数体内,标识符arguments是指向实参对象的引用,实参对象是一个类数组对象。通过数字下标就能访问传入函数的实参值,而不是非要通过名字来得到实参:

假设函数f的形参只有一个x,如果调用时传入两个实参,第一个实参可以通过参数名x来获得,也可以通过arguments[0]来得到。第二个实参只能通过arguments[1]来得到。

 

length属性  传递给函数的参数个数(即实参个数)(比如,传入两个参数,arguments.length就是2)

callee属性指代当前正在执行的函数。有时非常有用,比如在匿名函数中通过callee来递归调用自身。

例子:

let foo = function (x) {

  if (x <= 1) return 1;

  return x * arguments.callee(x-1);

};

 

caller属性指代调用当前正在执行的函数的函数。通过这个属性,可以访问调用栈。

 

实参对象有一个重要的用处,就是让函数可以操作任意数量的实参。 

 

将对象属性用做实参

当一个函数包含超过三个形参时,对于程序员来说,要记住调用函数中实参的正确顺序实在让人头疼。最好通过名值对的形式来转入参数,这样参数的顺序就无关紧要了。

做法:定义函数的时候,传入实参都写入一个单独的对象之中,在调用的时候传入一个对象,对象中的名值对是真正需要的实参数据。(例子178页)

 

实参类型

当一个方法可以接收任意数量的实参时,在参数的注释中,可以使用省略号:

function max(/* number... */) {}

 

有些情况下,应当添加类似的实参类型检查逻辑,因为宁愿程序在传入非法值时报错,也不愿非法值导致程序在执行时报错,相对而言,逻辑执行时的报错消息不甚清晰且更难处理。

 

作为值的函数

在JavaScript中,函数不仅是一种语法,也是值,也就是说,可以将函数赋值给变量,存储在对象的属性或者数组的元素中,作为参数传入另外一个函数等。

例子:

function square(x) { return x*x }  // 创建一个新的函数对象,并将其赋值给变量square。

函数还可以赋值给其他的变量,并且仍可以正常工作:

let s = square;  // 现在s和square指代同一个函数

square(4);  // =>16

s(4);  // =>16

 

除了可以将函数赋值给变量,同样可以将函数赋值给对象的属性:

let o = {square: function(x)  { return x*x }};  // 对象直接量

let y = o.square(16);  // y 等于 256

 

函数甚至不需要带名字,当把他们赋值给数组元素时:

let a = [function(x)  { return x*x }, 20];  // 数组直接量

a[0](a[1]);  // => 400  这句代码虽然看起来很奇怪,但的确是合法的函数调用表达式

 

作为命名空间的函数

为了防止变量污染,可以直接定义一个匿名函数,并在单个表达式中调用他:

(function() { // 匿名函数的表达式
// 模块代码
}()); // 结束函数定义并立即调用他


这种定义匿名函数并立即在单个表达式中调用他的写发非常常见,已经成为一种惯用法了。注意上面的圆括号的用法,function之前的左圆括号是必需的,因为入如果不写这个圆括号,JavaScript解释器会试图将关键字function解析为函数声明语句。使用圆括号JavaScript解释器才会正确地将其解析为函数定义表达式。使用圆括号是习惯用法,尽管有些时候没有必要也不应当省略。这里定义的函数会立即调用。

 

自定义函数属性

函数可以拥有属性。当函数需要一个“静态”变量来在调用时保持某个值不变,最方便的方式就是给函数定义属性,而不是定义全局变量,显然定义全局变量会让命名空间变得更加杂乱无章。
比如,假设你想写一个返回一个唯一整数的函数,不管在哪里调用函数都会返回这个整数。而函数不能两次返回同一个值,为了做到这一点,函数必须能够跟踪它每次返回的值,而且这些值的信息需要在不同的函数调用过程中持久化。可以将这些信息存放到全局变量中,但这并不是必须的,因为这个信息仅仅是函数本身用到的。最后将这个信息保存到函数对象的一个属性中,下面这个例子就实现了这样一个函数,每次调用函数都会返回一个唯一的整数:

foo.num = 1;
function foo() {
  foo.num++;
}
foo();
foo.num;  // => 2

 

闭包

函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。

所有JavaScript函数都是闭包:他们都是对象,他们都关联到作用域链

函数的执行用到了作用域链,这个作用域链是函数定义的时候创建的!和在哪里调用是没有关系的!这个特性可以捕捉倒局部变量(和参数),并一直保存下来,看起来像这些变量绑定到了在其中定义他们的外部函数。

词法作用域的规则:函数定义时的作用域链到函数执行时依然有效。

例子(1):

let scope = 'global scope';  // 全局变量

function foo() {

  let scope = 'local scope';  // 局部变量

  function f() { return scope; }  // 在作用域中返回这个值

  return f();

}

foo()  // => local scope

 

例子(2):

let scope = 'global scope';  // 全局变量

function foo() {

  let scope = 'local scope';  // 局部变量

  function f() { return scope; }  // 在作用域中返回这个值

  return f;

}

foo()();  // => local scope

 

闭包可以捕捉到单个函数调用的局部变量,并将这些局部变量用做私有状态:

let uniqueInteger = (function () {

  let counter = 0;

  return function () { return counter++ };

}());

粗略来看,第一行代码看起来像将函数赋值给一个变量uniqueInteger,实际上,这段代码定义了一个立即执行的函数,因此是这个函数的返回值赋值给了变量uniqueInteger。

现在,我们来看函数体,这个函数返回另一个函数,这是一个嵌套函数,我们将它赋值给变量uniqueInteger,嵌套的函数是可以访问作用域内的变量,而且可以访问外部函数中定义的counter变量。

当外部函数返回之后,其他任何代码都无法访问counter变量,只有内部的函数才能访问到它。

 

length属性

定义函数时的形参个数 

 

prototype属性

每一个函数都包含一个prototype属性,这个属性是指向一个对象的引用,这个对象称做“原型对象”(prototype object)。

每一个函数都包含不同的原型对象。

当将函数用做构造函数的时候,新创建的对象会从原型对象上继承属性。

 

call()和apply()方法

我们可以将call()和apply()看作是某个对象的方法,通过调用方法的形式来间接调用函数。

第一个实参是要调用函数的母对象,他是调用上下文,在函数体内通过this来获得对他的引用。

对于call()来说,第一个调用上下文实参之后的所有实参就是要传入待调用函数的值。

apply()有点类似,但传入实参的形式和call()有所不同,他的实参都放入一个数组当中。

注意:传入apply()的参数数组可以是类数组对象也可以是真实的数组。实际上,可以将当前函数的arguments数组直接传入(另一个函数的)apply()来调用另一个函数

例子:

let o = { x:1 };

function foo() { return this.x; }

foo.call(o);  //=>1

foo.apply(o);  //=>1

和下面代码的功能类似(假设对象o中预先不存在名为m的属性)

o.m = foo;  // 将f存储为o的临时方法

o.m();  // 调用方法

delete o.m;  // 将临时方法删除

 

bind()方法

将函数绑定至某个对象,返回一个新的函数。传入新函数的任何实参都将传入原始函数

例子:

function f(y) { return this.x + y }  // 这个是待绑定的函数

let o = { x:1 };  // 将要绑定的对象

let g = f.bind(o);  // 通过调用g()来调用o.f()

g(2);  //=> 3

其他:除了第一个实参之外,传入bind()的实参也会绑定至this。

 

Function()构造函数

函数还可以通过Function()构造函数来定义

例子:

let f = new Function( "x", "y", "return x*y" );

等价于下面的代码:

let f = function(x, y) { return x*y };

 

注意:Function()构造函数并不需要通过传入实参以指定函数名。就像函数直接量一样,Function()构造函数创建一个匿名函数。

posted @ 2020-10-31 18:11  或许从前  阅读(91)  评论(0)    收藏  举报