【JS】【Core】函数,函数调用上下文this,闭包

函数调用的上下文 —— this

搜索本文中的this关键字。

————————————————————————————————————————————————————————————————————————————


函数的定义会为形参提供实参的值。函数使用它们实参的值来计算返回值,称为该函数调用表达式的值。除了实参之外,每次调用还会拥有另一个值——本次调用的上下文——这就是 this 关键字

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

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

JS的函数可以嵌套在其他函数定义中,这样它们就可以访问它们被定义时所处的作用域中的任何变量。这意味着JS函数构成了一个闭包(closure),它给JS带来了非常强劲的编程能力。

函数定义

函数使用 function 关键字来定义;它可以用在函数定义表达式或者函数声明语句里。在两种形式里,函数定义都从 function 关键字开始,其后跟随这些组成部分:

  • 函数名字标识符 —— 函数名称是函数声明语句必须的部分。它的用于就像变量的名字,新定义的函数对象会赋值给这个对象。对函数定义表达式来说,这个名字是可选的;如果存在,该名字只存在与函数体中,并指代该函数对象本身。
  • 一对圆括号 —— 其中包含由0个或者多个逗号隔开的标识符组成的列表。这些标识符是函数的参数名称,它们就像函数体中的局部变量一样。
  • 一对花括号 —— 其中包含0条或多条JS语句。这些语句构成了函数体;一旦调用函数,就会执行这些语句。

例如:(函数声明;函数表达式

// 函数声明语句
function printprops(o){
   ... ...
}

// 函数表达式
var square = function(x ){ ... ...}

// 函数表达式可以包含名称
var f = function fact(x) { ... ...};

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

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

注意:

  • 以表达式方式定义的函数,函数的名称是可选的。
  • 一条函数声明语句实际上声明了一个变量,并把一个函数对象赋值给它。
  • 相对而言,定义函数表达式时并没有声明一个变量。
  • 如果一个函数表达式包含名称,函数的局部作用域将会包含一个绑定到函数对象的名称,实际上,函数的名称将会称为函数内部的一个局部变量。
  • 以表达式方式定义的函数在定义之前无法调用
  • 函数声明语句被提前到外部脚本或外部函数作用域的顶部,所以以这种方式声明的函数,可以被在它定义之前出现的代码所调用

函数返回值

大多数函数包含一条 reuturn 语句。return语句导致函数停止执行,并返回它的表达式的值给调用者。

如果return语句没有一个与之相关的表达式,则它返回undefined值。如果一个函数不包含return语句,那它就只执行函数体中每条语句,并返回undefined值给调用者。

函数命名

通常函数名的第一个字符为小写

嵌套函数

在JS里,函数可以嵌套在其他函数里。

例如:

function hypotenuse(a, b){
    function square(x) { return x*x;}
    return Math.sqrt(square(a) + square(b));
}


嵌套函数可以访问嵌套它们的函数的参数和变量。


函数调用

构成函数主体的JS代码在定义时并不会执行,只有调用该函数时,才会执行。

有4中方式来调用JS函数:

  • 作为函数
  • 作为方法
  • 作为构造函数
  • 通过它们的call()和apply()方法间接调用

函数调用

以函数形式调用的函数通常不使用 this 关键字。

方法调用

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

属性访问表达式由两部分组成:一个对象和属性名称。在这样的方法调用表达式中,对象为调用上下文,函数体可以使用关键字 this 引用该对象

例如:

var calculator = {
    operand1 : 1,
    operand2: 1,
    add: function(){
        this.result = this.operand1 + this.operand2;
    }
};

calculator.add();
calculator.result; //=> 2

任何函数只要作为方法调用实际上都会传入一个隐式的实参 —— 这个对象。


嵌套的函数不会从调用它的函数中继承this。

如果想访问这个外部函数的this值,需要将this的值保存在一个变量里,这个变量和内部函数都同在一个作用域内。通常使用变量self来保存this。

例如:

var o = {
    m: function(){
        var self = this;
        console.log(this === o); //=> true
        f();

        function f(){
            console.log(this === 0); // false
            console.log(self === o); // true
        }
    }
};

构造函数调用

如果函数或者方法调用之前带有关键字new,它就构成构造函数调用。

如果构造函数调用在圆括号内包含一组实参列表,先计算这些实参表达式,然后传入函数内。但如果构造函数没有形参,JS构造函数调用的语法是允许省略实参列表和圆括号的。

例如:

var o = new Object();
var o = new Object;


构造函数可以使用 this 关键字来引用这个新创建的对象。

构造函数通常不使用 return 关键字。然而如果构造函数显示地使用 return 语句返回一个对象,那么调用表达式的值就是这个对象。如果构造函数使用return语句但没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用这个对象作为调用结果。

间接调用

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


函数的实参和形参

JavaScript中的函数定义并未指定函数形参的类型,函数调用也未对传入的实参值做任何类型检查。JavaScript函数调用甚至不检查传入形参的格式

可选形参

当调用函数的时候传入的实参比函数声明时指定的形参个数要少,剩下的形参都将设置为undefined值。

应当给省略的参数赋一个默认值。

例如:

function getPropertyNames(o, a){
    if (a === undefined) a=[];
    或者
    a = a || [];
    ... ...
}

当用这种可选实参来实现函数时,需要将可选实参放在实参列表的最后。

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

当调用函数的时候传入的实参个数超过函数定义时的形参个数时,没有办法直接获得命名值的引用。

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

例如:

function f(x, y, z){
    if (arguments.length != 3){
        ... ...
    }
}

第一个实参可以通过参数名x来获得,也可以通过 arguments[0] 来得到。

将对象属性用作实参


实参类型

JS方法的形参并未声明类型,在形参传入函数体之前也未做任何类型检查。

可以给实参补充注释,以此使代码文档化;对于可选的实参来说,可以在注释中补充一下“这个实参是可选的”。当一个方法可以接收任意数量的实参时,可以使用省略号。


作为值的函数

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


自定义函数属性

函数可以拥有属性。当函数需要一个“静态”变量来在调用时保持某个值不变,最方便的方式就是给函数定义属性,而不是定义全局变量。

例如:

// 由于函数声明被提前了,因此这里是可以在函数声明之前给它的成员赋值的
uniqueInteger.counter = 0;

function uniqueInteger() {
    return uniqueInteger.counter++;
}


作为命名空间的函数

在JS中是无法声明只在一个代码块内可见的变量的,基于这个原因,我们尝尝简单地定义一个函数用作临时的命名空间,在这个命名空间内定义的变量都不会污染到全局命名空间。

例如:

function mymodule(){
    // 模块代码
    // 这个模块所使用的所有变量都是局部变量
    // 而不是污染全局命名空间
}
mymodule(); // 不要忘了还要调用这个函数


还可以直接定义匿名函数,例如:

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

这种定义匿名函数并立即调用的写法是一种惯用法。

function之前的圆括号是必须的,因为如果不写这个左括号,JS解释器会试图将关键字function解析为函数声明语句。

这里的函数会立即调用。


闭包 - Closure

闭包的正确称谓是 first-class function with lexical scope。

  • First-class function 决定了函数可以在另一个函数内部被定义,并且作为 return value 返回
  • Lexcial scope 指:当一个名称不是参数也不是局部变量的时候,VM 会从该函数定义的函数运行时的局部变量中绑定。如果仍然没有再向上类推(最终所有函数都是在 global chunk 运行时被定义,所以最后都会从 global 变量中绑定)

Structure and Interpretation of Computer Programming 的第三章的 environment model 是对闭包最精确的描述。每个函数被定义时有一个 bound environment,每个函数每次被调用时有一个 created environment。一个函数定义时的 bound environment 是这个函数的外层函数被调用时的 created environment。局部变量在 created environment 中,闭包变量在 bound environment 中。

看一个例子:

var foo = ( function() {
    var secret = 'secret';
    // “闭包”内的函数可以访问 secret 变量,而 secret 变量对于外部却是隐藏的
    return {
        get_secret: function () {
            // 通过定义的接口来访问 secret
            return secret;
        },
        new_secret: function ( new_secret ) {
            // 通过定义的接口来修改 secret
            secret = new_secret;
        }
    };
} () );

foo.get_secret ();                // 得到 'secret'
foo.secret;                       // Type error,访问不能
foo.new_secret ('a new secret');  // 通过函数接口,我们访问并修改了 secret 变量
foo.get_secret ();                // 得到 'a new secret'


引用 Douglas Crockford:

闭包是指在 JavaScript 中,内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后。

需要注意的一点时,内部函数访问的是被创建的内部变量本身,而不是它的拷贝。所以在闭包函数内加入 loop 时要格外注意。另外当然的是,闭包特性也可以用于创建私有函数或方法。

函数定义时候引用的变量的值在定义的时候并没有被评估出来,只有等到执行的时候才评估出其具体的值。经典的例子就是那个for循环里面定义一些函数,这些函数变量里面引用了循环的计数器,等到这些函数执行的时候,其循环计数器是循环后的。 

var d = new Array; 
for(var i=9;i>0;i--){d[i] = function (){alert(i);}} 

其中d这个数组里面的函数就是闭包,其包含了对外部变量的引用。当你再执行d这个数组里面的所有函数的时候,就会发现这个时候全部都是0。


关于为什么在 JavaScript 中闭包的应用都有关键词“return”,引用 JavaScript 秘密花园中的一段话:

闭包是 JavaScript 一个非常重要的特性,这意味着当前作用域总是能够访问外部作用域中的变量。 因为函数 是 JavaScript 中唯一拥有自身作用域的结构,因此闭包的创建依赖于函数


函数属性,方法和构造函数

函数也是对象,它们也可以拥有属性和方法。

length属性

函数的length属性是只读属性,它代表函数实参的数量,这里的参数指的是“形参”而非“实参”,也就是函数定义时给出的实参个数,通常也是在函数调用时期望传入函数的实参个数。


prototype属性

这个属性是指向一个对象的引用,原型对象。每一个函数都包含不同的原型对象。当将函数用作构造函数的时候,新创建的对象会从原型对象上继承属性。


call()方法和apply()方法

可以将call()和apply()看作是某个对象的方法。call()和apply()的第一个实参是要调用函数的母对象,它是调用上下文,在函数体内通过this来获得对它的引用。

例如,要想以对象o的方法来调用函数f(),可以这样使用:

f.call(o);
f.apply(o);

与下面的代码的功能类似:

o.m = f;    // 将f存储为o的临时方法
o.m();      // 调用它,不参入参数
delete 0.m; // 将临时方法删除

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

例如:

f.call(o. 1. 2);


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

例如:

f.apply(o, [1, 2]);

bind()方法

这个方法的主要作用就是将函数绑定至某个对象。当在函数f()上调用bind()方法并传入一个对象o作为参数,这个方法将返回一个新的函数。调用新的函数将会把原始的函数f()当作o的方法来调用。传入新函数的任何实参都将传入原始函数。

例如:

function f(y) { return this.x + y;}
var o = {x : 1};
var g = f.bind(o);
g(2)   // => 3

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


toString()方法


Function()构造函数

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

var f = new Function("x", "y", "return x*y;");
等价于
var f = function(x, y){ return x*y; }

Function()构造函数可以传入任意数量的字符串实参,最后一个实参所表示的文本就是函数体;传入构造函数的其他所有的实参字符串是指定函数的形参名字的字符串。如果定义的函数不包含任何参数,只须给构造函数简单地传入一个字符串——函数体即可。

Function()构造函数允许JS在运行时动态地创建并编译函数。

Function()构造函数在实际编程中很少会用到。


可调用的对象














评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值