JavaScript函数

1 函数定义

  • 使用function关键字来定义,即function fName(para,...){ statment;...;},可使用在函数声明语句函数定义表达式这两种形式中
    • 函数名称标识符fName。

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

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

    • 一对圆括号(),其中包含由0个或者多个用逗号隔开的标识符组成的列表。这些标识符是函数的参数名称,它们就像函数体中的局部变量一样

    • 一对花括号{ },其中包含0条或多条JavaScript语句。这些语句构成了函数体:一旦调用函数,就会执行这些语句

  • 使用函数声明语句定义函数
    • 实际上声明了一个变量, 并把一个函数对象赋值给它
    • 函数可以命名, 这个名称用来来指代自己,成为函数内部的一个局部变量
    • 这种方式定义的函数,拥有函数提升特性,即函数声明语句 被“提升” 到外部脚本或外部函数作用域的顶部, 使之可以被它定义之前的代码所调用
  • 使用 表达式方式定义函数
    • 函数名称通常被省略,以使定义代码更紧凑,如果不省略函数名称, 函数的局部作用域将会包含一个绑定到函数对象的名称。 实际上, 函数的名称将成为函数内部的一个局部变量
    • 这种方式定义的函数,即要对一个变量进行赋值,而赋值不会提升,所以这种方式定义函数,必须先定义后调用
  • return与函数返回值
    • 函数中return 语句没有一个与之相关的表达式或省略了return, 则它返回undefined值给调用者
    • 没有return 语句的函数,有时也叫过程
  • 函数嵌套
    • 作用域规则:被嵌套函数可以访问嵌套函数的参数和变量
    • 使用函数声明的函数,不可出现在条件语句、循环语句、try/cache/finally及with语句中
    • 使用表达式定义的函数,可以出现在任何JavaScript语句中
//函数声明
function myFunc(name,age){
    return 'My name is ' + name + ', My old is' +age+'.' ;
}
//函数定义表达式
var f = function(name){
    return  'My name is ' + name;
}
//或者
var f = function fName(name){
    return  'My name is ' + name;
}
//无return
function myalter(msg){
  alter(msg);
}
//函数嵌套
function fName(x,y){
  function func(a,b){
    return a + b;
  }
  return func(x,y);
}

 2 函数调用

  • 作为函数
    • 非严格模式的ECMAScript 5对函数调用的规定, 调用上下文(this的值)是全局对象

    • 在严格模式下,调用上下文则是undefined

    • 以函数形式调用的函数通常不使用this关键字。但 “this” 可以用来判断当前是否是严格模式
//定义并调用一个函数来确定当前脚本运行时是否为严格模式
var strict = (function() { return !this; }()); 
  • 作为方法
    • 任何函数作为方法调用,实际上都会传入一个隐式的实参(即方法调用的母体对象)。通常,基于哪个对象的方法可以进行多种操作
    • 方法和this关键字是面向对象编程的核心,当方法不需要返回值时,如果返回this,即方法调用的母体对象,就可形成链式调用
    • this是关键字, 不是变量, 也不是属性名。 JavaScript的语住不允许给this赋值
    • 和变量不同, 关键字this没有作用域的限制, 嵌套的函数不会从调用它的函数中继承 this
      •  如果嵌套函数作为方法调用, this指向调用它的对象
      •  如果嵌套函数作为函数调用, 非严格模式下this指向全局对象,严格模式下this的值是undefined
      • 调用嵌套函数时
        • this指向不是调用它的外层函数的上下文
        • 要访问调用它的外层函数的this值, 可将this的值保存在和内部函数都同在一个作用域内的变量里,通常使用变量self来保存this
var obj = {
    method:function(){
        self = this;//将this保存到同一作用域的self变量中
        console.log(this === obj);//true
        innerF();
    } 
    function innerF(){//嵌套函数
        console.log(self == obj);//true
        console.log(this == obj);//是全局变量或unfined
    } 
}
  • 作为构造函数
    • 构造函数调用和普通的函数调用以及方法调用在实参处理、 调用上下文和返回值方面都有不同
    • 如果构造函数调用时,包含一组实参列表, 则会先计算这些实参表达式,然后传入函数内,这和函数调用和方怯调用是 致的

    • 但如果构造函数没有形参,JavaScript构造函数调用的语法允许省略实参列表和圆括号的(凡是没有形参的构造函数调用都可以省略圆括号)

    • 构造函数调用创建一个新对象,这个对象继承自构造函数的prototype属 性。构造函数试图初始化这个新创建的对象, 并将这个对象作为其调用上下文,因此构造函数可以使用this关键字来引用这个新创建的对象。尽管构造函数看起来像一个方法调用,但它依然会将这个新对象作为调用上下文
    • 构造函数通常不使用return关键字,而是通过初始化新对象, 在构造函数的函数体执行完毕时,将其显式返回,在这种情况下, 构造函数调用表达式的计算结果就是这个新对象的值
    • 如果构造函数显式地使用return语句返回了一个对象,那么调用表达式的值就这个新对象
    • 如果构造函数使用return语句没有指定返回值,或者返回的是一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果。
  • 通过函数的方法call()或者apply()间接调用

3 函数的形参与实参

  • JavaScript中的函数定义未指定函数形参的类型
  •  JavaScript函数调用也不对传人的实参值做任何类型检查, 甚至不检查传入形参的个数
  • 可选形参
    • 当调用函数的时候传人的实参比函数声明时指定的形参个数要少,剩下的形参都将设置 为undefined值
    • 调用函数时形参是否可选以及是否可以省略,应当保持较好的适应性
      • 使用 if 语句,判断形参是否为undefined
      • 使用 ||(如var para = para || 0) 
    • 当用这种可选实参来实现函数时,需要将可选实参放在实参列表的最后。
  • 可变长的实参列表:实参对象
    • 引入实参对象可解决当调用函数的时候传入的实参个数超过函数定义时的形参个数时,不能直接获得未命名值的引用问题

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

    • 实参对象的重要的用处,就是让函数可以操作任意数量的实参
    • arguments的callee属性与caller属性,callee指代当前正在执行的函数(如指代匿名函数),caller指代当前调用正在执行的函数的函数(非标准)
    • 将对象属性用作实参(编程风格)
      • 通过名/值对的形式来传入参数, 参数的顺序无关紧要
      • 允许在函数中设置省略参数的默认值
function opt(options){
    options = options || {};
    var para1 = options.x || 0;
    var para2 = options.y || 12;
    var para3 = options.name || 'John';
    var para4 = options.old || 5;          
}
  • 实参类型:必要时,还是需要对传入参数执行类型检查以避免出错

4 作为值的函数

  • 函数可以定义,也可以调用,这是函数最重要的特性。函数定义和调用是JavaScript的词法特性,对于其他大多数编程语言来说亦是如此
  • 在JavaScript中,函数不仅是一种语法,也是值
    • 可以将函数赋值给变量
    • 可存储在对象的属性或数组的元素中
    • 可作为参数传入另外一个函数等
  • 自定义函数属性
    • JavaScript中的函数并不是原始值, 而是一种特殊的对象, 也就是说, 函数可以拥有属性
    • 当函数需要一个 “静态” 变量来在调用时保持某个值不变, 最方便的方式就是给函 数定义属性, 而不是定义全局变量
//返回一个唯一整数的函数, 不管在哪里调用函数都会返回这个整数,
//而函数不能两次返回同一个值, 函数就必须能够跟踪它每次返回的值, 
//而且这些值的信息需要在不同的函数调过程中持久化。
//可以将这些信息存放到全局变量中, 但这并不是必需的,
//因为这个信息仅仅是函数本身用到的。
//最好做法是将这个信息保存到函数对象的一个属性中
fAttr.counter = 0;
function fAttr(){
    return fAttr.counter++;//先返回属性值,然后自增1
}

 5 作为命名空间的函数

  • 利用JavaScript中的函数作用域规则
    • 在函数中声明的变量在整个函数体内都是可见的(包括在嵌套的函数中), 在函数的外部是不可见的
    •  不在任何函数内声明的变量是全局变量, 在整个JavaScript程序中都是可见的
    •  在JavaScript中是无法声明只在一个代码块内可见的变量
  • 基于这些规则,我们常常简单地定义一个函数用做临时的命名空间, 在这个命名空间内定义的变量都不会污染到全局命名空间
  • 作为命名空间的函数可以解决
    • 变量冲突
    • 函数内定义的变量都是函数的局部变量
  • 作为命名空间的函数定义后必须通过调用才能获得命名空间
    • 作为函数调用
    • 使用自调用函数

6 闭包

  • JavaScript也采用词法作用域(lexicalscoping)
    • 函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的
    • 为了实现这种词法作用域,JavaScript函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链
    • 函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为“闭包
  • 从技术的角度讲,所有的JavaScript函数都是闭包:它们都是对象且都关联到作用域链。定义大多数函数时的作用域链在调用函数时依然有效
  • 当调用函数时,闭包所指向的作用域链和定义函数时的作用域链不是同一个作用域链时
    • 典型应用:当一个函数嵌套了另外一个函数,外部函数将嵌套的函数对象作为返回值返回
    • 这类嵌套的函数闭包是一种强大的编程技术,在JavaScript中很常见
var scope = 'global scope';
function checkscope(){
    var scope = 'local scope';
    function f(){
        return scope;
    }
    return f();//返回被嵌套函数的执行结果
}
console.log(checkscope());//local scope
//-----------------------------修改后
var scope = 'global scope';
function checkscope(){
    var scope = 'local scope';
    function f(){
        return scope;
    }
    return f;//直接返回被嵌套函数定义
}
console.log(checkscope()());//local scope 说明scope变量的作用域是在函数被定义时就确定了(这就是词法作用域),并且在函数被调用时依然有效
  • 实现闭包
    • 理解闭包: 函数定义时的作用域 链到函数执行时依然有效
    • 对于类似C语言这种史底层的编程语言来说,外部函数中定义的局部变量在函数返回后就不存在了,这是基于栈的CPU架构:如果一个函数的局部变量定义在CPU的栈中, 那么当函数返回时它们的确就不存在了
    • 对于JavaScript语言将作用域链描述为一个对象列表, 不是绑定的栈。 每次调用函数时, 都会为之创建一个新的对象用来保存局部变量, 把这个对象添加至作用域链中。 当函数返回的时候,就从作用域链中将这个绑定变量的对象删除
      •  如果不存在嵌套的函数, 也没有其他引用指向这个绑定对象, 它就会被当做垃圾回收掉
      •  如果定义了嵌套的函数, 每个嵌套的 函数都各自对应一个作用域链, 并且这个作用域链指向一个变量绑定对象
        • 如果这些嵌套的函数对象在外部函数中被保存下来, 那么它们也会和所指向的变量绑定对象一样当做垃圾回收
        • 如果这个函数定义了嵌套的函数, 并将它作为返回值返回或者存储在某处的属性里, 就存在一个外部引用指向这个嵌套的函数。 它就 不会被当做垃圾回收,并且它所指向的变量绑定对象也不会被当做垃圾回收
//外层函数自执行,counterF获得的是外层函数的返回值
//函数返回值是一个嵌套函数,它可以访问作用域内的变量,也可以访问外层函数定义的变量counter
//当外层函数返回后,其他代码便无法访问counter了,只有内部的嵌套函数可以访问
var counterF = (function(){
    var counter = 0;
    return function(){
      return counter++;
    }
})();

//象counter这样的私有变量不是只能用在一个单独的闭包内
//在同一个外部函数内定义的多个嵌套函数也可以访问它
//这多个嵌套函数都共享一个作用域链
function counter(){
    var n = 0;//这个变量也可以直接使用函数形参即counter(n),这种方式可以为共享的私有变量n设置初始值
    return function(){
        count:function(){return n++;},
        reset:function(){n = 0;}
    };
}
var c = counter(),d = counter();
c.count();//0
d.count();//0
c.reset();
c.count();//0,执行了reset
d.count();//1,没有执行reset,可见c,d互不影响,相互独立
//counter()函数返回了一个 “计数器” 对象,这个对象包含两个方法:
//count()返回一个整数,reset()将计数器重置为内部状态
// 这两个方法都可以访问私有变量n
//每次调用counter()都会创建一个新的作用域链和一个新的私有变量
//调用counter()两次, 得到两个计数器对象,而且彼此包含不同的私有变量
//调用其中一个计数器对象的count()或reset()不会影响到另外一个对象。

//使用闭包技术来共享的私有状态的通用做法
//定义私有变量,嵌套两个函数,一个获取私有变量值,一个设置私有变量值
function addPrivateProperty(o, name, predicate){
    var value;//这是一个属性值
    o[”get"+ name] = function() { return value; };//getname嵌套函数
    o[”set"+ name] = function(v) {
        if (predicate && !predicate(v)){ 
            throw Error( ”set”+name+” :invalid value ”+ v);
        }
        else {
            value = v;
        };//setname嵌套函数
var o = {};
addPrivateProperty(o, "Name", function(x){return typeof x==="string";} );
o.setName("Mike");
console.log(o.getName());//Mike
o.setName(9);//setName:invalid value 9

//关联到闭包的作用域链都是 “活动的” ,嵌套的函数不会将作用域内的私有成员复制一份, 也不会对所绑定的变量生成静态快照(staticsnapshot)
//返回一个函数组成的数组,它们的返回值是0 ~ 9 
function constfuncs(){
    var funcs = [];
    for(var i = o; i < 10; i++)
    funcs[i] = function() { return i; }; 
    return funcs;
}
var funcs = constfuncs ();
funcs[5] () //10,不是我们希望的5,这就是这10个嵌套函数都共享这个i 
    • 每个函数调用都包含一个this值, 如果闭包在外部函数里是无法访问this的值 , 除非外部函数将this转存为一个变量
    • arguments并不是一个关键字, 但在调用每个函数时都会自动声明它, 由于闭包具有自己所绑定的arguments,因此闭包内无法直接访问外部函数的参数数组,除非外部函数将参数数组保存到另外一个变量中
var self = this;
var outerArgumets = arguments;

 7 函数的属性

  • length属性
    • 在函数体里, arguments.length表示传入函数的实参的个数
    • 函数本身的length属性是只读属性, 它代表函数形参的数量, 通常也是在函数调用时期望传入函数的实参个数
    • 判断arguments.length与arguments.callee.length是否相等,即直到定义参数个数与调用传入参数是否相等
  • prototype属性
    • 每一个函数都包含一个prototype属性,这个属性是指向一个对象的引用,这个对象称做“原型对象”(prototype object)
    • 每一个函数都包含不同的原型对象
    • 当将函数用做构造函数的时候, 新创建的对象会从原型对象上继承属性

8 函数的方法

  • 函数的call()方法和apply()方法
    • 函数的call()和apply()可以看做是某个对象的方法,通过调用方法的形式来间接调用函数(Object. prototype.toString方法,用以输出对象的类)
    • call()和apply()的第一个实参是要调用函数的母对象,它是调用上下文,在函数体内通过this来获得对它的引用
    • 在ECMAScript 5的严格模式中,call()和apply()的第一个实参都会变为this的值,哪怕传入的实参是原始值甚至是null或undefined
    • 在ECMAScript 3和非严格模式中,传入的null和undefined都会被全局对象代替, 而其他原始值则会被相应的包装对象(wrapper object)所替代
    • 对于call()来说,第一个调用上下文实参之后的所有实参就是要传入待调用函数的值
    • apply()方法和call()类似,但传入实参都放入一个数组当中,如果一个函数的实参可以是任意数量,给apply()传入的参数数组可以是任意长度的,传入apply()的参数数组可以是类数组对象也可以是真实数组
  • bind()方法
    • bind()是在ECMAScript5中新增的方法,主要作用就是将函数绑定至某个对象
    • 当在函数f()上调用bind()方法并传入一个对象作为参数,这个方法将返回一个新的函数
    • (以函数调用 的方式)调用新的函数将会把原始的函数f()当做对象的方法来调用。传入新函数的任何实参都将传入原始函数
    • ECMAScript 5中的bind()方法不仅仅是将函数绑定至一个对象,除了第一个实参之外, 传入bind()的实参也会绑定至thi s, 这个附带的应用是一种常见的函数式编程技术, 有时也被称为 “柯里化” (currying )
function f(y) { return this.x + y; }
var o = { x : 1 };
var g = f.bind(o);
g(2) // => 3

//返回一个函数,通过调用它来调用o中的方法f(), 传递它所有的实参
function bind(f,o){
    if (f.bind) return f.bind(o);
    else return function(){
        return f.apply(o, arguments);//绑定函数
    }
} 


var sum= function(x,y) { return x + y }; 
var succ = sum.bind(null, 1); 
succ(2) // => 3: x绑定到1,并传入2作为实参y

function f(y,z) { return this.x + y + z }; 
var g = f.bind({x:1}, 2); 
g(3)// 6: this.x绑定到1, y绑定至2, z绑定到3
  • toString()方法 
    • 大多数(非全部)的toString ()方陆的实现都返回函数的完整源码
    • 内置函数往往返回一个类似“[native code]” 的字符串作为函数体
  • Function()构造函数
    • Function()构造函数可以传入任意数量的字符串实参,最后一个实参所表示的文本就是函数体
      • 函数体可以包含任意的JavaScript语句,每两条语句之间用分号分隔
      • 传入构造函数的其他所有的实参字符串是指定函数的形参名字的字符串
      • 如果定义的函数不包含任何参数,只须给构造函数简单地传入一个字符串即函数体
    • Function()构造函数允许JavaScript在运行时动态地创建井编译函数
      • 每次调用Function()构造函数都会解析函数体,井创建新的函数对象
      • 如果是在一个循环或者多次调用的函数中执行这个构造函数,执行效率会受影响
      • 相比之下, 循环中的嵌套函数和函数定义表达式则不会每次执行时都重新编译
    • 关于Function()构造函数非常重要的一点,就是它所创建的函数并不是使用词法作用域,相反,函数体代码的编译总是会在顶层函数执行
    • Function()构造函数并不需要通过传入实参以指定函数名。 就像函数直接量一样,Function()构造函数创建一个匿名函数
    • 可以将Function()构造函数认为是在全局作用域中执行的eval(), eval()可以在自己的私有作用域内定义新变量和函数
    • Function()构造函数在实际编程过程中很少会用到
  • 可调用的对象
    • 与 “类数组对象” 并不是真正的数组, 函数也存在类似的情况。 “可调用的对象” (callable object)是一个对象, 可在函数调用表达式中调用它

    •  所有的函数都是可调用的,但并非所有的可调用对象都是函数

      • 诸如Window.alert()和Document.getElementsByld(),使用了可调用的宿主对象, 而不是内置函数对象

      • 另外一个常见的可调用对象是RegExp对象, 可以直接调用RegExp对象,这比调用它的exec()方法更快捷一些

9 函数式编程

  • JavaScript井非函数式编程语言, 但在JavaScript中可以像操控对象一样操控函数,也就是说可以在JavaScript中应用函数式编程技术
  • ECMAScript 5中的数组方法(诸如map()和reduce())就可以非常适合用于函数式编程风格

9.1 使用函数处理数组

//使用数组的方法map()和reduce()函数,计算平均值、标准差等
var data = [1,1,3,5,5];
var sum = function(x,y){
    return x + y;
};
var square = function(x){
    return x*x;
};
var mean = data.reduce(sum) / data.length;
var deviations = data.map(function(x){return x - mean;});
var stddev = Math.sqrt(deviations.map(square).reduce(sum) / (data.length - 1));

9.2 高阶函数

  • 所谓高阶函数(higher-order function)就是操作函数的函数,它接收一个或多个函数作为参数,井返回一个新函数

 

posted @ 2023-06-04 20:53  好记心不如烂笔头  阅读(21)  评论(0编辑  收藏  举报