码迷,mamicode.com
首页 > 编程语言 > 详细

Javascript – 函数

时间:2014-11-02 19:28:54      阅读:350      评论:0      收藏:0      [点我收藏+]

标签:http   io   ar   os   使用   java   sp   strong   文件   

转自:http://www.tootei.net/archives/471

目录

1函数概述

Javascript 中函数的内涵非常丰富,在讨论底层原理之前,先总结一下之前提到过的有关函数的概念。

1.1 函数与对象

对象是已命名的数据集合,它是比数组更抽象和广义的结构。对象中的已命名的数据,被称为对象的属性,其名称便是属性名。特别地,如果属性的值为函数,称之为对象方法。

在 Javascript 中,函数是内部实现了 [[Call]] 方法的可执行对象,它继承了 Object 对象的内置属性

function a (){ }
alert( a instanceof Object ) ; // true , a 是 Object 的实例
alert( a.constructor === Function ) ; // true , constructor 继承自 Object
alert( a.hasOwnProperty( "prototype" ) ) ; // true , hasOwnProperty 继承自 Object
 

函数的属性 Function.prototype ,用于在构造对象的时候,设置对象的原型属性(Object.__proto__)。

var a = { x : 1 } ;
function func (){ } ;
func.prototype = a ;
var b = new func() ;
alert(b.__proto__ == func.prototype ) ; // true
 

函数的属性 Function.caller 代表调用它的函数。如果是从全局域中调用函数,它的 Function.caller 属性为 null

function a(){
    return a.caller ;
}
function b(){
    return a() ;
}
alert( a() == null ) ; // true
alert( b() == b ); // true
 

函数的属性 Function.length 代表函数定义时的形参个数。

function a(){ }
function b( x , y , z ) { }
alert( a.length ) ; // 0
alert( b.length ) ; // 3
 

函数的方法 applycall,执行函数,并使用指定的对象作为函数体中 this 指代的对象。

function func( v1 , v2 ){
    this.x = v1 ;
    this.y = v2 ;
    this.toString = function(){
        return "x : " + this.x + ", y : " + this. y ;
    }
}
func.apply( document ,[ 5 , 6 ] );
alert( document ) ;
func.call( document , 7 , 8  );
alert( document ) ;
 

这两个方法的具体区别和应用请看下面的 函数的动态调用

从对象的角度考虑,函数作为数据,可以进行声明和赋值,可以作为参数传递、作为执行结果返回。

function createHandler( callback ){ 作为参数
    return function(){ // 作为返回值
        alert( "Step 1" );
        process( callback  ) ; 
    }
}
function process( callback ){
    alert( "Step 2" ) ;
    if(typeof callback == "function")
        callback() ;
}
function end(){
    alert( "Step 3" );
}
var a = createHandler( end ); // 赋值
a( ) ; // "Step 1"、"Step 2"、"Step 3"
 

1.2 创建函数

创建函数的几种方式,请查看 创建函数

注意:”函数声明(Function Declaration)” 和 “函数创建(Function Creation)” 是不同的概念。”函数声明” 特指用 function 声明的方式创建函数,而 “函数创建” 还包括其他的定义函数的方式。以下说到 “函数声明” 的时候,不包括 “函数表达式(Function Expression)”。

一旦使用用 “函数声明” 的方式创建了函数,就可以在其定义域内的任何地方调用它。

alert( add( 1 , 2 ) ) ; // 3
function add( x , y ){
    return x + y ;
}
 

需要一提的是,浏览器加载代码需要一定的时间,Javascript 以 <script> 块为单位、从上到下一块一块地加载和执行代码。同样是上面的例子,如果放在不同的 <script> 块内,就会报错:

<script type="text/javascript">
    a() ; // 报错 a
</script>
<script type="text/javascript">
    function a(){ }
</script>
 

如果有两个文件 a.jsb.jsb.js 中调用了 a.js 中定义的全局函数 fa.js 必须在 b.js 之前引入的 Html 中,否则就会报错:

<script type="text/javascript" src="a.js"></script>
<script type="text/javascript" src="b.js"></script>
 

1.3 参数

1.3.1 形参与实参

形参(形式参数),是出现在函数定义中的参数列表。实参(实际参数),是函数调用时实际传入的参数列表。
function add( x , y ){ // x , y 是形参
    x = +x || 0 ;
    y = +y || 0 ;
    return x + y ;
}
alert( add() ) ;  
alert( add( 1 ) ); 
alert( add( 1 , 2 ) ); 
alert( add( 1 , 2 , 3 ) ); // 1 , 2 , 3 都是实参
 

可以看出,形参和实参的数目可以不同:

  • 如果实参少于形参,多出来的形参的值就是 undefined ;
  • 如果实参多于形数,多出来的实参不能通过形参标识符来访问。
function add( x , y ){ // x , y 是形参
    alert( "x = " + x + " , y = " + y) ;
}
add() ;  // "x = undefined , y = undefined"
add( 1 ) ; // "x = 1 , y = undefine"
add( 1 , 2 ); // "x = 1 , y = 2"
 

1.3.2 Aruments 对象

在函数体内,可以通过 arguments 对象、用数字下标的方式访问所有实参。
function add(){
    var a = arguments[0] || 0 ;
    var b = arguments[1] || 0 ;
    alert( a + b ) ;
}
add() ;  // "0"
add( 1 ) ; // "1"
add( 1 , 2 ); // "3"
 

arguments 对象不是数组,对它不能调用数组的一些方法( joinsort 等 )。

arguments 对象形式如下所示:

arguments = { 0 : val0 , 1 : val1 , 2 : val2 , ... , length : argumentsNumber , callee : func }
 

arguments 对象的类型为 Arguments

  • Arguments.length 是其内包含实参的个数。
  • Arguments.callee 是当前函数的引用。

对于没有名称的匿名函数,可以使用 Arguments.callee 实现递归调用:


(function( i ){
    if( i > 0 ){
        alert( i ) ;
        arguments.callee( --i ) ;
    }
})( 5 ) ;
 

一个使用 Argument 对象模拟函数重载的例子:

function Point(){
    if( arguments.length == 0 ){ // 如果没有传入参数
        this.x = 0 ;
        this.y = 0 ;
    }else if( arguments.length < 2 ){ // 传入一个参数
        var p = arguments[ 0 ] ;
        if( p instanceof Point ){ // 参数是 Point 类型
            this.x = p.x ;
            this.y = p.y ;
        }else if( typeof p == "number" || p instanceof Number){ // 参数是个数字
            this.x = Number( p ) ;
            this.y = 0 ;
        }else{
            throw new TypeError( "参数类型错误 !" );
        }
    }else if( arguments.length == 2 ){ // 传入两个参数
        var x = arguments[ 0 ] ;
        var y = arguments[ 1 ] ;
        if( ( typeof x == "number" || x instanceof Number ) && 
                        ( typeof y == "number" || y instanceof Number )){ // 两个参数都是数字
            this.x = x ;
            this.y = y ;
        }else{
            throw new TypeError( "参数类型错误 !" );
        }
    }else{ // 传入多个参数
        throw new TypeError( "参数类型错误 !" );
    }
}
var a = new Point() ; // a : { x : 0 , y : 0 }
var b = new Point( 5 ) ; // b : { x : 5 , y : 0 }
var c = new Point( 1 , 3 ) ; // c : { x : 1 , y : 3 }
var d = new Point( b ) ; // d : { x : 5 , y : 0 }
// new Point( null ) ; // 报错
 

1.4 调用

1.4.1 函数的调用者

前面介绍函数对象的方法时,已经介绍过这一概念 Function.caller

由于 Function.caller 不属于 ECMAScript 标准的一部分,尽管有浏览器实现了它,还是尽量避免使用这个属性。

1.4.2 函数的所有者

函数中的 this 指代函数的所有者,也就是调用这个函数的对象。

不同的使用情境,this 指代的对象不同。底层的原理,请看下面的 This,这里总结一下一般情况:

函数作为对象的方法

一般来说,函数所在的对象就是它的所有者:

function addPro( x , y ){
    this.x = x ;
    this.y = y ;
}
var a = { init: addPro };
var b = { init: addPro };
a.init( 1 , 2 ); // a => { init : addPro , x : 1 , y : 2 }
b.init( 3 , 4 ); // b => { init : addPro , x : 3 , y : 4 }
 

如果没有使用对象运算符 .[ ] 来调用对象方法,其中的 this 不指代其所在对象:

var a = { init: function(){ 
    this.x = "xstr" ;
} } ;
var b = a.init ; 
b() ; 
alert( a.x ) ; // "undefined"
alert( window.x ) ; // "xstr"
 

函数作为构造函数

构造函数的所有者,就是构造函数所创建的对象。

function obj(){
    this.x = "adding x"; // 为构造的对象添加属性 x 
}
var a = new obj() ; // a => { x : "adding x" }
 

全局范围内定义的变量和函数

在浏览器客户端,全局的变量和函数的所有者是 window。

var a = "a in window" ;
function b(){
    alert("function in window");
}
alert( window.a ) ; // "a in window"
window.b() ; // "function in window"
 

如果没有明确的所有者,那它就是指向 window

function outer(){
     function inner(){
        this.x = "String from inner call.";
    }
    inner();
}
outer() ;
alert( window.x ) ; // ""String from inner call.""
 

1.4.3 函数的动态调用

函数对象的方法 Function.applyFunction.call,使用指定的对象作为函数体中 this 指代的对象,立即执行函数。

Function.callFunction.apply 的参数是可选的。

如果有参数的话,第 1 个参数代表了函数运行时 this 所指代的对象。

  • 对于 call 来说,后面的 0 个或多个参数依次作为参数传给函数;
  • 对于 apply 来说,第 2 个参数必须是数组,其元素依次作为参数传给函数。
function init( x , y , z ){
    switch( arguments.length ){
        case 3 : // 3 个参数的时候,依次为对象添加属性 z、y、x
        this.z = z ;
        case 2 : // 2 个参数的时候,依次为对象添加属性 y、x
        this.y = y ;
        case 1 : // 1 个参数的时候,依次为对象添加属性 x
        this.x = x ;
    } 
}
var a = {} , b = {} ;
init.call( a , 1 , 2 ) ; // a => { y : 1 , x : 2 }
init.apply( b , [ 1 , 2 , 3 ] ) ; // b => { z : 1 , y : 2 , x : 3 }
 

2执行上下文栈(Execution context stack)

Javascript 有三种可执行代码:全局代码、函数代码、eval 代码。Javascript 中的每一句代码,都是在与其相关的执行上下文(Execution context,简称 EC )中进行解析和求值的。

全局的执行上下文只有一个。程序一开始的时候,先进入这个上下文中,然后再解析和运行代码。

函数和 eval 的执行上下文可以有多个:每次调用函数或 eval ,都会产生一个新的上下文。

function foo( bar ) {} 
// 调用同一个函数,每次生成不同的上下文,保存相关的状态
// 这里能看到的状态是参数 bar 的值
foo(10);
foo(20);
foo(30);
 

一个执行上下文可以激活另一个执行上下文,比如在函数中调用函数。这在逻辑上,构成了 执行上下文栈

一个上下文,如果激活了其他的上下文,就叫做 caller;被激活的上下文,叫做 calleecallee 被激活时,caller 挂起自身的上下文,并将控制传递给 calleecallee 则被置于上下文栈的顶端,成为活动的执行上下文(Active Execution Context)。callee 执行完成后,再将控制返回 callercaller 继续解析和执行接下来的代码,直到结束。

bubuko.com,布布扣

由于程序一开始就进入了全局上下文,所以执行上下文栈的最下方是全局执行上下文(Global EC)。

如果全局上下文中激活了一个函数的上下文 EC1 ,下图展示了 EC1 进栈和出栈过程中执行上下文栈的变化:

bubuko.com,布布扣

2.1 执行上下文(Execution context)

执行上下文可以看做是普通的对象,它拥有一些必要的属性,以跟踪相关代码的执行。下图展示了执行上下文的结构:

bubuko.com,布布扣

除了 Variable objectScope chainthisValue 这三个必要的属性外,执行上下文可能包含额外的状态,这依赖于它的实现。

2.1.1 变量对象(Variable object)

变量对象(Variable object)是相关执行上下文的数据容器,它与上下文相关,存储上下文中的所有变量声明和函数声明。

注意:函数表达式(和函数声明相对)不包含在变量对象(Variable object)之中。

变量对象(Variable object)是一个抽象的概念。不同的上下文类型中,代表它的对象是不同的。比如在全局上下文中,变量对象(Variable object)就是全局对象本身(window),所以才能以 window 属性的方式访问全局域中的变量声明和函数声明:

var a = "variable" ;
alert( window.a ) ; "variable"
 

需要注意的是,变量对象(Variable object)作为执行上下文的属性,存在于函数调用之后、代码运行之前。Javascript 扫描函数的代码(不包括嵌套函数),将用 var 声明的变量和用 function 声明的函数登记在变量对象(Variable object)中。其中:

  • var 声明只登记标识符,值为 undefined
  • 函数声明,会导致数据绑定;
  • 如果函数中出现同名的 var 声明 和 function 声明,取后者的绑定结果。
var foo = 10;
 
function bar() {} // 函数声明
(function baz() {}); // 函数表达式
 

上面的代码中,全局变量对象(Global VO)应该是这个样子:

bubuko.com,布布扣

从变量对象的角度来看,局部变量都是 VO 的属性。所以函数内重复的 var 声明是无效的,只有第一个 var 声明起作用。而重复的函数声明,会被最后一个声明覆盖。

function a(){
   var x = 5 ;
   var x ;
   alert( x ) ; // 5
   function y(){ alert( "func1" ) ;}
   y() ; // "func2"
   function y(){ alert( "func2" ) ;}
   y() ; // "func2"
}
a() ;
 

上面的例子中,重复声明变量 x 是无效的,它并没有变成 undefined ;重复声明函数 y ,导致任意位置的函数引用都是相同的 —— 因为 var 声明 和 function 声明在函数体未运行前就已经确定了。

需要注意的是,如果函数表达式右边是一个命名函数,它的名字同样不会登记在 VO 中。所以外部不能访问这个名字,只能在函数表达式内部引用。

var a = functon b (){
   alert(typeof b) ; 
}
alert( typeof b ) ; // "undefined"
a() ; // "function"
 

2.3 活动对象(Activation object)

函数被调用后,一个叫做 “活动对象(Activation object)” 的特殊对象就被创建了。

活动对象(Activation object)被形参、arguments 对象、var 声明和 function 声明填充。其中:

  • 每个形参对应着活动对象的一个属性,属性值为传入实参的值;未传入实参的情况下是 undefined
  • arguments 引用是一个 "类数组" 的对象,包含所有实参值;

上面说到变量对象(Variable object),强调它是一个抽象的概念。在全局上执行下文(Global Execution Context)中,变量对象(Variable object)就是全局对象 window

活动对象(Activation object)就是函数执行上下文中的变量对象。
function foo(x, y) {
  var z = 30;
  function bar() {} // 函数声明
  (function baz() {}) ; 函数表达式
} 
foo(10, 20);
 

函数的活动对象(AO)如下图所示:

bubuko.com,布布扣

再次强调,函数表达式 baz 不包含在变量对象中。

2.4 作用域链(Scope chain)

作用域链(Scope chain)就是对象的列表,用于查询出现在代码中的标识符。

一般情况下,作用域链就是所有父变量对象,加上当前函数自身的变量对象。但有些情况下,代码在运行中会将一些对象加到作用域链的前端,比如 with 谓词,catch 从句。

函数的内部属性 [[Scope]] 中,存储了所有的父变量对象。执行上下文中的 Scope Chain 相当于:

ExecutionContext.ScopeChain = VariableObject.Concat( Function.[[Scope]] ) ;
//其中
Function.[[Scope]] = [ ... ,Variable Object 2 , Variable Object 1 , Global Object ] ;
 

作用域链的最后,是全局对象 window

一个标识符在上下文中,可能代表一个变量的名称、一个函数声明、一个形参等。如果函数代码中出现了一个不在当前变量对象中的标识符,就称之为自由变量。查询自由变量,就用到了作用域链。

对象的原型链 一样,如果变量没有在自身的作用域中找到,就顺着作用域链(Scope chain)查询父变量对象(Variable object)。

var x = 10; 
( function foo() {
  var y = 20 ;
  ( function bar() {
    var z = 30 ;     
    console.log( x + y + z ) ; // "x" and "y" 是 "自由变量"
  } )();
} )();
 

理解作用域链有几点要注意:

1. 函数一创建,Javascript 就将其父作用域赋给其内部属性 [[Scope]]。 这里的 “创建” ,指的是运行时的创建,而不是书面意义上的声明。对于函数表达式(Function Expression)来说,其创建要等到它所在的语句被执行后才能完成;对于函数声明(Function Declaration),所在作用域生成变量对象后,其内所有的函数声明都完成了创建;

2. Javascript 是单线程的,任意时刻只能做一件事情。不管代码处于多深的位置,总要从全局域开始一层一层地执行。函数的内部属性 [[Scope]] 在这过程中,被传递、保存,构成执行上下文对象中作用域链;

3. 作用域链是变量对象的列表,对象是引用类型。函数的内部属性 [[Scope]]保存是变量对象的引用。

4. [[Scope]] 属性在初始化后,不会变动,直到函数销毁,。

3闭包(Closures)

闭包(Closures)就是 函数保存在函数中的作用域 的结合体。通过已保存的作用域,可以引用自由变量。
function foo() {
    var x = 10 ;
    return function bar() {
        console.log( x ) ;
    } ;
}
var returnedFunction = foo() ; 
var x = 20 ; 
returnedFunction(); // 10
 

上面的例子中,返回的函数 bar ,通过其内部属性 [[Scope]] 保存了外部函数 foo 的作用域。

foo 的作用域由变量对象 fooAO 和全局对象 window 组成。fooAO 中包含变量 x ,window 也包括变量 x 。但是 fooAO 处于作用域链前端,所以 console.log(x) ; 输出了 10 而不是 20

var x = 10; 
function foo() {
    console.log( x ) ;
} 
(function (funArg) {
  var x = 20;
  funArg(); // 10 
})( foo ) ; 
 

上面的例子中,函数 foo 在声明的时候,它的内部属性 [[Scope]] 被初始化为全局作用域 window 。其内部遇到自由变量时,只能去全局对象 window 中寻找。这种情况下,看似有标识符冲突,其实没有:如果全局域中没有定义 x ,会直接报错 "x is not defined" 的错误。

function baz() {
  var x = 1 ;
  return {
        foo : function foo() { return ++x ; },
        bar : function bar() { return --x ; }
    } ;
} 
var closures = baz() ; 
console.log(
  closures.foo() , // 2
  closures.bar()  // 1
) ;
 

上面的例子中,返回对象的 foo 方法和 bar 方法各自保存了父作用域(父作用域中包含 [ bazAO , global object ] ),所以可以继续访问到自由变量 x

4This

This 的值是与执行上下文相关的对象,所以它又被称为上下文对象,更确切地说,this 决定了执行上下文在哪一个对象上激活。任何对象都可以作为 this 的值。

注意:this 是执行上下文的属性,而不是变量对象的属性。

与变量相比,this 的值不参与标识符的裁决过程、不会进行作用域链的查找,而是直接从执行上下文中取值。This 的值在进入上下文的时候就确定了,并且不再改变。

4.1 全局域中的 this

在全局上下文中,this 的值就是全局对象(也是全局的变量对象)。

var x = 10 ; 
console.log(
  x , // 10
  this.x , // 10
  window.x // 10
) ;
 

4.2 函数中的 this

在函数体中,this 的值与调用方式相关:

function foo() {
    alert( this ) ;
}
foo() ; // global object
foo.prototype.constructor() ; //foo.prototype

var bar = {
  baz : foo
} ;
bar.baz() ; // bar

( bar.baz )() ; // bar
( bar.baz = bar.baz )(); // global object
( bar.baz, bar.baz )(); // global object
( false || bar.baz )(); // global object

var otherFoo = bar.baz;
otherFoo(); // global object
 

这其中的规律或者原理是什么呢?为了理解 this 的判定过程,需要认识一个内部类型: "引用"( Reference type )。

4.3 引用

Javascript 中有值类型和引用类型。其中值类型存储的就是它的值;而引用类型存储的只是对象的地址。

将引用类型的数据赋给一个变量,会产生一个引用,并将它赋给变量。

引用,涉及到名称的绑定,只可能发生在两种情况下:

  1. 处理标识符
  2. 处理对象的属性访问器

所以,要将纯粹的 "值" 与 "名称" 区别来看。

var a = 0 ;
var b = function(){} ;
var c = { x : 2 } ;
 

其中,abcx 属于名称,0function(){}2{ x : 2 } 属于值。

从执行上下文的角度来看,Javascript 代码中每一个名称都和对象关联着:要么属于全局变量对象(Global Object),要么属于函数的活动对象(Active Object),要么属于某一个常量对象( { x : 1} ),要么属于内置对象或自定义对象。

用伪码来表示引用,形式如下:

var valueOfReferenceType = {
  base : <base object> ,
  referencedName: <referenced name>
} ;
 

其中,base 就是名称所在的对象,referencedName 就是名称的字符串。

标识符可能是变量名、函数名、参数名、意外的全局对象属性名 [1],比如:

var foo = 10 ;
function bar() { }
 

Javascript 内部操作的中间结果如下:

var fooReference = {
    base : global ,
    referencedName : ‘foo‘
} ; 
var barReference = {
    base : global ,
    referencedName : ‘bar‘
} ;
 

为了获取到引用的值,有一个 GetValue 方法,伪码表示如下:

function GetValue( value ) { 
    if ( Type( value ) != Reference ) {
        return value;
    } 
    var base = GetBase( value ); 
    if ( base === null ) {
        throw new ReferenceError;
    } 
    return base.[[Get]]( GetReferencedName( value ) ) ;  
}
 

对象的内部方法 [[Get]] 返回对象属性的真实值,这个过程会查询对象的原型链。

GetValue( fooReference ) ; // 10
GetValue( barReference ) ; // function object "bar"
 

标识符是对象属性访问器,比如:

var foo = { bar : function {} } ;
foo.bar() ; // 或者 foo[ ‘bar‘ ]() ;
 

表示成内部的引用类型,伪码如下:

var fooBarReference = {
    base : foo ,
    referencedName : ‘bar‘
} ; 
GetValue(fooBarReference)() ; 
 

那么,"引用" 类型和函数上下文中的 this 有什么关联呢?

函数上下文中的 this 值,由调用者 caller 提供,并取决于当前的调用表达式(即如何书写函数的调用)。

 

  • 如果函数调用运算符 ( ) 的左边是一个引用,this 的值就是其 base 对象。
  • 如果函数调用运算符 ( ) 的左边不是一个引用,this 的值 null。
  • 如果活动对象(AO)作为引用的 base 对象,this 的值 null。
  • 由于 null 赋给 this 毫无意义,它被隐式地转换成全局对象( window )。

再来看上面的例子:

function foo() {
    alert( this ) ;
}
foo() ; // global object 
// ∵ foo => { base : global object }

foo.prototype.constructor() ; // foo.prototype 
// ∵ constructor => { base : foo.prototype }

var bar = {
  baz : foo
} ;
bar.baz() ; // bar  
// ∵ baz => { base : foo.bar }

( bar.baz )() ; // bar  
// ∵ 分组符号不进行任何运算

( bar.baz = bar.baz )(); // global object   
// ∵ 赋值表达式的值 = 右边表达式的值。
// 它是真实的值,不是引用

( bar.baz, bar.baz )(); // global object
// 逗号表达式的值 = 是最后一个表达式的值。
// 它是真实的值,不是引用。

( false || bar.baz )(); // global object
// "逻辑或" 表达式的值 = 第一个操作数 ? 第一个操作数 :第二个操作数 
// 它是真实的值,不是引用

var otherFoo = bar.baz;
otherFoo() ; // global object

function outer(){
    var inner = foo ;
    inner () ;
}
outer() ; // global object
// ∵ inner 属于 outerAO,this 的值为 null => global object
 
除此之外:

 

  1. 构造函数中的 this 值为刚创建的对象。
  2. 可以使用函数的 Function.call 和 Function.apply 方法调用函数,传入一个对象作为函数体中 this 的值。

引用

ECMA-262-3 in detail. Chapter 3. This.

[1] 意外的全局对象属性名,示意如下:

function a (){
    xxxx = 5 ; // 意外的全局对象属性名
}
a() ;
alert( window.xxxx ) ; // 5

Javascript – 函数

标签:http   io   ar   os   使用   java   sp   strong   文件   

原文地址:http://www.cnblogs.com/kangzhibao/p/4069712.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!