javascript模式笔记3——函数

Posted by 徐璇 on December 5, 2013

1.函数

特点1. 函数是第一类对象(first-class object)

  • 可以在运行时动态创建,还可以在程序执行过程中创建
  • 可以分配给变量,可以将其引用复制到其他变量,可以被扩展,甚至可以被删除(除少数情况外)
  • 可以作为参数传递给其他函数,也可以由其他函数返回
  • 可以有自己的属性和方法

特点2. 函数提供作用域

  • 块不创建作用域,js中仅存在函数作用域
  • if for while的{ }中,使用var声明的变量,对于包装函数来说才是局部变量

1.1函数3类型

  • 函数声明
  • 命名函数表达式
  • 未命名函数表达式
//函数声明(function declaration)
function add(arg){}


//命名函数表达式(named function expression)
var add = function add(arg){};


//未命名函数表达式(unnamed function expression),也简称为函数表达式
//匿名函数(anonymous function)
var add = function(arg){};

1.2函数的命名属性

函数有个只读属性name,虽然不是标准属性,但是在很多环境中都可以使用它。在函数声明和命名函数表达式中,已定义了name属性。在匿名函数表达式中,依赖于其实现方式,name属性可能是未定义,也能是空字符串。

function foo(){}
var bar = function(){}
var baz = function baz(){}

console.log(foo.name);//foo
console.log(bar.name);//""
console.log(baz.name);//baz

var foo = function bar(){ };
使用命名函数表达式将其分配给一个具有不同名称的变量,在技术上可行,但在一些浏览器中,这种行为没有被正确的实现,不推荐。

1.3函数的提升

function foo() {
    console.log("global foo");
}

function bar() {
    console.log("global bar");
}

function hoistMe(){
    console.log(typeof foo); //function
    console.log(typeof bar); //undefined

    foo(); //local foo
    bar(); //TypeError: undefined is not a function

    //函数声明
    //变量foo和函数实现都被提升
    function foo(){
        console.log("local foo");
    }

    //函数表达式
    //变量bar被提升————typeof bar里的bar是local的bar,而不是global的bar,所以输出undefined
    //函数实现没有被提升————所以执行bar();报错
    var bar = function(){
        console.log("local bar");
    };
}


hoistMe();

hoistMe()函数中的foo和bar,声明被提到的顶部,覆盖了全局的foo和bar。

两者区别:

函数 函数声明是否被提升 函数定义是否被提升
函数声明 yes yes
函数表达式 yes no

2.API模式(API patterns)

为函数提供更好的接口

  • 回调模式(Callback patterns)——将函数作为参数传递
  • 配置对象(Configuration objects)——控制参数数量
  • 返回函数(Returning functions)——当一个函数的返回值是另一个函数时
  • curry化(Currying)——新函数是基于现有函数,并加上部分参数列表创建时

2.1回调模式(Callback patterns)

function writeCode(callback){
    //...
    callback();
    //...
}

function introduceBugs(){
    //...
}

writeCode(introduceBugs);

注意:introduceBugs()作为参数传递给writeCode()是不带括号的;
括号表示要执行函数,在这种情况下,我们只需要传递该函数的应用,让writeCode()在适当的时候来执行它(也就是说,返回以后回调)

回调示例:

var findNodes = function () {
    var i = 100000, // big, heavy loop
    nodes = [], // stores the result
    found; // the next node found
    while (i) {
        i -= 1;
        // complex logic here...
        nodes.push(found);
    }
    return nodes;
};


var hide = function (nodes) {
    var i = 0, max = nodes.length;
    for (; i < max; i += 1) {
        nodes[i].style.display = "none";
    }
};
// executing the functions
hide(findNodes());

这个实现是低效的,hide()必须再次循环遍历由findNodes()返回的数组节点。如果能在findNodes()中实现隐藏逻辑,虽然高效,但是检索和修改逻辑耦合,它就不再是一个通用函数。

解决办法就是采用回调模式,代码如下:

// refactored findNodes() to accept a callback
var findNodes = function (callback) {
    var i = 100000,
    nodes = [],
    found;
    // check if callback is callable
    if (typeof callback !== "function") {
        callback = false;
    }
    while (i) {
        i -= 1;
        // complex logic here...
        // now callback:
        if (callback) {
            callback(found);
        }
        nodes.push(found);
    }
    return nodes;
};


// a callback function
var hide = function (node) {
    node.style.display = "none";
};
// find the nodes and hide them as you go
findNodes(hide);

findNodes()执行的唯一额外任务就是检查是否提供了可选的回调函数,如果存在的话就执行;
由于回调函数是可选的,所以重构后的findNodes()仍然可以像以前一样使用。这样,hide()的实现就简单多了,不需要循环遍历所有节点。

回调函数也可以是一个匿名函数:

// passing an anonymous callback
findNodes(function (node) {
    node.style.display = "block";
});

回调与作用域

问题的提出:回调函数不是一次性的匿名函数或全局函数,而是对象的方法,如果该回调方法使用this来引用它所属的对象,会导致一些意外情况…

var myapp = {};
myapp.color = "green";
myapp.paint = function(node){
    node.style.color = this.color;
}


var findNodes = function(callback){
    var node = document.getElementById("xx");
    if(typeof callback === "function"){
        callback(node);
    }
}


findNodes(myapp.paint);

findNodes()是一个全局函数,因此this指向全局对象,所以this.color没有被定义。如果findNodes()是一个名为dom的对象的方法(dom.findNodes()),那么回调内部的this指向dom,而不是预期的myapp。

this 关键字的用法其实比较复杂,不过你只要牢记一句话就可以:
“this变量永远指向函数运行时所在的对象,而不是函数被创建时所在的对象。如果处在匿名函数中、或者不处于任何对象中,this都指向宿主的根对象(在浏览器里面就是 window)”

解决方案:传递回调函数,还传递该回调函数所属的对象

var myapp = {};
myapp.color = "green";
myapp.paint = function(node){
    node.style.color = this.color;
}


var findNodes = function(callback, callback_obj){

    var node = document.getElementById("xx");

    if(typeof callback === "function"){
        callback.call(callback_obj, node);
    }

    if (typeof callback === "string") {
        callback_obj[callback].call(callback_obj, node);
    }
}


findNodes(myapp.paint, myapp);
findNodes("paint", myapp);

注意:以下实现方式,字体颜色也会发生变化,findNodes函数的参数myapp.paint(document.getElementById("xx"))就是在执行一个函数,这个函数中的this指向的是myapp

var myapp = {};
myapp.color = "green";
myapp.paint = function(node){
    node.style.color = this.color;
}


var findNodes = function(callback){
    if(typeof callback === "function"){
        callback();
    }
}


findNodes(myapp.paint(document.getElementById("xx")));

其他实现方式:

__bind = function(fn, me){
    return function(){
        return fn.apply(me, arguments);
    };
};


var myapp = {};
myapp.color = "green";
myapp.paint = function(node){
    node.style.color = this.color;
}


var findNodes = function(callback, callback_obj){
    var node = document.getElementById("xx");
    if(typeof callback === "function"){
        //指定callback的执行环境为callback_obj
        callback = __bind(callback, callback_obj); 
        callback(node);
    }
}


findNodes(myapp.paint, myapp);
__bind = function(fn, me){
    return function(){
        return fn.apply(me, arguments);
    };
};


var myapp = {
    color : "green",
    paint : function(node){
        node.style.color = this.color;
    }
};


//这里的myapp不可以写成this,this指向全局对象
myapp.paint = __bind(myapp.paint, myapp); 


var findNodes = function(callback){
    var node = document.getElementById("xx");
    if(typeof callback === "function"){
        callback(node);
    }
}


findNodes(myapp.paint);

__bind = function(fn, me){
    return function(){
        return fn.apply(me, arguments);
    };
};


function myapp(){
    this.color = "green";
    //这里的this是myapp
    this.paint = __bind(this.paint, this); 
}


myapp.prototype.paint = function(node){
    node.style.color = this.color;
}


var findNodes = function(callback){
    var node = document.getElementById("xx");
    if(typeof callback === "function"){
        callback(node);
    }
}


var myappInstance = new myapp();
findNodes(myappInstance.paint);

ES5为所有的Function对象引入一个新的bind方法,它实现下面的行为:

var myapp = {};
myapp.color = "green";
myapp.paint = function(node){
    node.style.color = this.color;
}


var findNodes = function(callback, callback_obj){

    var node = document.getElementById("xx");

    if(typeof callback === "function"){
        // fn = callback.bind(callback_obj);
        // fn(node);
        callback.bind(callback_obj)(node);
    }
}


findNodes(myapp.paint, myapp);

比较下两者用法:

var __bind = function(func, thisValue) {
    return function() {
        return func.apply(thisValue, arguments);
    }
}


var person = {
    name: "Alex Russell",
    hello: function() { 
    	console.log(this.name + " says hello world"); 
    }
}


// $("#some-div").click(person.hello.bind(person));
$("#some-div").click(__bind(person.hello, person));

2.2配置对象模式(Configuration patterns)

使用一个参数对象替代所有参数,将该参数称为conf,即配置的意思。
优点:控制参数数量。

var conf = {
    username: "batman",
    first: "Bruce",
    last: "Wayne"
};
addPerson(conf);

2.3返回函数(Returning functions)

var setup = function () {
    alert(1);
    return function () {
        alert(2);
    };
};

var my = setup(); // alerts 1
my(); // alerts 2
var setup = function () {
    var count = 0;
    return function () {
        return (count += 1);
    };
};

var next = setup();
next(); // returns 1
next(); // 2
next(); // 3

2.4curry化(Currying)

2.4.1函数调用&函数应用

函数调用 called/invoked
函数应用 applied,使用方法 Function.prototype.apply()来应用函数

//sayHi()是全局函数,可以直接调用
var sayHi = function(who){
    return "Hello" + (who ? ", " + who : "") + "!";
}


//函数调用(called or invoked)
console.log(sayHi());  //Hello!
console.log(sayHi("kathy"));  //Hello, kathy!


//函数应用(applied),使用方法Function.prototype.apply()
//第一个参数 - 将要绑定到该函数内部this的一个对象,如果为null,this-->全局对象
//第二个参数 - 一个数组或多个参数变量,这些参数将变成可用于该函数内部的类似数组的arguments对象
console.log(sayHi.apply(null, ["kathy"])); //Hello, kathy!

第一个apply()传递alien引用,内部的this指向alien对象,this.age是alien的age
第二个apply()传递null引用,内部的this指向全局对象,this.age是全局的age

//sayHi()是一个对象的方法,必须通过改对象调用
var age = 20;
var alien = {
    age : 15,
    sayHi : function(who){
        return "Hello" + (who ? ", " + who : "") + "! And you're " + this.age + " year's old ?";
    }
};


console.log(alien.sayHi("xuxuan")); //Hello, xuxuan! 
console.log(alien.sayHi.apply(alien,["xuxuan"]));  //Hello, xuxuan! And you're 15 year's old ?
console.log(alien.sayHi.apply(null,["xuxuan"]));  //Hello, xuxuan! And you're 20 year's old ?


console.log(sayHi("xuxuan"));    //报错:sayHi未定义
console.log(sayHi.apply(alien,["xuxuan"])); //报错:sayHi未定义
console.log(sayHi.apply(null,["xuxuan"]));  //报错:sayHi未定义

注意callapply的区别:
Function.prototype.call是建立在apply上的“语法糖”(syntax sugar): 当函数只有一个参数时,可以根据实际情况避免创建只有一个元素的数组。

//sayHi()是一个构造函数里的方法,必须通过实例调用
var age = 20;
var Alien = function(){
    this.age = 15;
    this.sayHi = function(who){
        return "Hello" + (who ? ", " + who : "") + "! And you're " + this.age + " year's old ?";
    };
};


var alien = new Alien();
console.log(alien.sayHi("xuxuan")); //Hello, xuxuan! And you're 15 year's old ?
console.log(alien.sayHi.apply(alien,["xuxuan"]));  //Hello, xuxuan! And you're 15 year's old ?
console.log(alien.sayHi.apply(null,["xuxuan"]));  //Hello, xuxuan! And you're 20 year's old ?


console.log(alien.sayHi.call(alien,"xuxuan")); //Hello, xuxuan! And you're 15 year's old ?
console.log(alien.sayHi.call(null,"xuxuan"));  //Hello, xuxuan! And you're 20 year's old ?

2.4.2部分应用(partial application)

部分应用向我们提供了另一个函数,随后再以其他参数调用该函数。
假想的partialApply()方法:

var add = function(x, y){
    return x + y;
}

//完全应用
add.apply(null, [5,4]);  //9

//部分应用
var newadd = add.partialApply(null,[5]);

//应用一个参数到函数中
newadd.apply(null,[4]);  //9

事实上,js没有partialApply()方法,默认也不会表现出上述类似行为。但是可以构造出这种行为,成为Curry过程。

2.4.3Curry化

function add(x, y){
    var oldx = x, oldy = y;
    //部分
    if(typeof oldy === "undefined"){
        return function(newly){
            return oldx + newly;
        }
    }
    //完全应用
    return x + y;
}


console.log(typeof add(5)); //function
console.log(add(3)(4));  //7


//add2000是一个新函数
var add2000 = add(2000);
console.log(add2000(13));  //2013

更为精简的实现版本:

function add(x, y){
    //部分应用
    if(typeof y === "undefined"){
        return function(y){
            return x + y;
        }
    }
    //完全应用
    return x + y;
}

更通用的方式,将任意函数转换成一个新的可以接受部分参数的函数:

function schonfinkelize(fn){
    var slice = Array.prototype.slice,
        stored_args = slice.call(arguments, 1);
    return function(){
        var new_args = slice.call(arguments),
            args = stored_args.concat(new_args);
        return fn.apply(null, args);
    };
}


//普通函数
function add(x, y){
    return x + y;
}


//将一个函数curry化以获得一个新的函数
var newadd = schonfinkelize(add, 5);
console.log(newadd(4));  //9


//另一种用法————直接调用新函数
console.log(schonfinkelize(add,5)(4)); //9


//---------------------------------------------------------------


//普通函数
function add2(a, b, c, d, e){
    return a + b + c + d + e;
}


//两步curry化
var addOne = schonfinkelize(add2, 1);
console.log(addOne(10, 10, 10, 10));  //41


var addTow = schonfinkelize(addOne, 2, 3);  //addOne函数只用传递4个参数
console.log(addTow(5,5));  //16


//可运行于任意数量的参数
console.log(schonfinkelize(add2, 1, 2, 3)(5, 5));  //16

curry使用场景:

  • 调用同一个函数,并且传递的参数绝大多数是相同的。
  • 通过将一个函数集合部分应用到函数中,从而动态创建一个新函数。这个新函数会保存重复的参数(所以,不用每次都传这些重复的参数),而且还会使用预填充原始函数所期望的完整参数列表。

3.初始化模式(Initialization patterns)

不污染全局命名空间,使用临时变量,以一种更加整洁、结构化的方式执行初始化以及设置任务

  • 即时函数(Immediate functions)——定义之后立即执行
  • 即时对象初始化(Immediate object initialization)——匿名对象组织了初始化任务,提供了可被立即调用的方法
  • 初始化时分支(Init-time branching)——帮助分支代码在初始化过程中仅检测一次

3.1即时函数模式(Immediate Function pattern)

两种写法:

(function () {
    alert('watch out!');
}());


(function () {
    alert('watch out!');
})();

作用:初始化代码提供了一个作用域沙箱(sandbox)。

使用场景:页面加载时,需要一些初始化工作,而且仅需要执行一次,没有理由去创建一个可复用的命名函数。但是代码也需要一些临时变量,初始化阶段完成后就不需要了。以全局变量形式创建变量是一个差劲的方式。这时候,即时函数就可以派上用场了,我们将所有代码包装到它的局部作用域,且不会将任何变量泄露到全局作用域中。

(function () {
    var days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
        today = new Date(),
        msg = 'Today is ' + days[today.getDay()] + ', ' + today.getDate();
    alert(msg); //Today is Tue, 26
}());

可以将参数传到即时函数中:

(function (who, when) {
    console.log("I met " + who + " on " + when);
}("Joe Black", new Date()));
//I met Joe Black on Tue Nov 26 2013 10:32:50 GMT+0800 (中国标准时间)

一般情况下,全局对象是以参数的方式传递给及时函数,以便于在不使用window指定全局作用域限定的情况下可以在函数内部方位该对象,这样使得代码在浏览器环境之外时具有更好的互操作性。

(function (global) {
    // access the global object via `global`
}(this));

即时函数的返回值:

//返回一个数值
var result = (function(){
    return 2 + 2;
}());


//另一种方式忽略包装函数的括号,因为将及时函数的返回值分配给一个变量时并不需要这些括号
var result = function(){
    return 2 + 2;
}();


//返回一个函数
var getResult = (function(){
    var res = 2 + 2;
    return function(){
        return res;
    };
}());
console.log(getResult);  //getResult是一个函数
/*
function (){
    return res;
}
*/
console.log(getResult());  //4


//当定义对象属性也可以用即时函数
//场景:需要定义一个对象在生命周期内永远不会改变的属性,但是在定义之前需要执行一些工作以找出正确的值,
//此时,使用即时函数包装这些工作,返回值将会成为属性值
var o = {
    message : (function(){
        var who = "me",
            what = "call";
        return what + " " + who;
    }()),
    getMsg : function(){
        return this.message;
    }
};

console.log(o.getMsg()); //call me
console.log(o.message); //call me

3.2即时对象初始化(Immediate object initialization)

保护全局作用域:
- 即时函数模式(Immediate Function pattern)
- 即时对象初始化(Immediate object initialization)
使用带有init( )方法的对象,该方法在对象创建后立即执行,init( )函数负责所有的初始化任务。

以下两种写法都可以:

({...}).init();
({...}.init());

下面是即时对象模式的一个栗子:

({
    // here you can define setting values
    // a.k.a. configuration constants
    maxwidth: 600,
    maxheight: 400,
    // you can also define utility methods
    gimmeMax: function () {
        return this.maxwidth + "x" + this.maxheight;
    },
    // initialize
    init: function () {
        console.log(this.gimmeMax());
        // more init tasks...
    }
}).init();

优点:
1、执行一次性的初始化任务时保护全局命名空间
2、使整个初始化过程更有结构化

缺点:
压缩问题,私有属性和方法不会被重命名为更短的名称。

注意:
这种模式只适用于一次性任务,之后没有对该对象的访问。如果要的话,可以在init( )尾部添加 return this;

3.3初始化时分支(Init-time branching)

也称为加载时分支(load-time branching),是一种一种优化模式。
当知道某个条件在整个程序声明周期内都不会发生改变的时候,仅对该条件测试一次是很有意义的。

此段代码效率低下,每次调用utils.addListenter( ) 或者 utils.removeListener( ) 都会重复地执行相同的查:

// BEFORE
var utils = {
    addListener: function (el, type, fn) {
        if (typeof window.addEventListener === 'function') {
            el.addEventListener(type, fn, false);
        } else if (typeof document.attachEvent === 'function') { // IE
            el.attachEvent('on' + type, fn);
        } else { // older browsers
            el['on' + type] = fn;
        }
    },
    removeListener: function (el, type, fn) {
        // pretty much the same...
    }
};

解决方式:
使用初始化时分支,在脚本初始化加载时一次性嗅探出浏览器特称

// AFTER
// the interface
var utils = {
    addListener: null,
    removeListener: null
};
// the implementation
if (typeof window.addEventListener === 'function') {
    utils.addListener = function (el, type, fn) {
        el.addEventListener(type, fn, false);
    };
    utils.removeListener = function (el, type, fn) {
        el.removeEventListener(type, fn, false);
    };
} else if (typeof document.attachEvent === 'function') { // IE
    utils.addListener = function (el, type, fn) {
        el.attachEvent('on' + type, fn);
    };
    utils.removeListener = function (el, type, fn) {
        el.detachEvent('on' + type, fn);
    };
} else { // older browsers
    utils.addListener = function (el, type, fn) {
        el['on' + type] = fn;
    };
    utils.removeListener = function (el, type, fn) {
        el['on' + type] = null;
    };
}

4.性能模式

加速代码运行

  • 备忘模式——使用函数属性存储计算结果
  • 自定义模式——以新的主体重写本身,是的在第二次或以后调用时仅需执行更少的工作

4.1备忘模式

给函数添加自定义属性,缓存函数结果,那么在下一次调用函数时,如果缓存在,就直接从缓存中取结果。就不用再做重复的繁琐的计算。

var myFunc = function (param) {
    if (!myFunc.cache[param]) {
        var result = {};
        // ... expensive operation ...
        myFunc.cache[param] = result;
    }
    return myFunc.cache[param];
};
// cache storage
myFunc.cache = {};

如果参数复杂,可以将其序列化,如果序列化为一个JSON字符串。
注意:在序列化过程中,对象的“标识”将会丢失,如果有两个不同的对象并且恰好都有相同的属性,这两个对象会共享同一个缓存条目。

var myFunc = function () {
    var cachekey = JSON.stringify(Array.prototype.slice.call(arguments)),
        result;
    if (!myFunc.cache[cachekey]) {
        result = {};
        // ... expensive operation ...
        myFunc.cache[cachekey] = result;
    }
    return myFunc.cache[cachekey];
};
// cache storage
myFunc.cache = {};

另一种方法使用arguments.callee,注意ES5不支持arguments.callee

var myFunc = function (param) {
    var f = arguments.callee,
    result;
    if (!f.cache[param]) {
        result = {};
        // ... expensive operation ...
        f.cache[param] = result;
    }
    return f.cache[param];
};
// cache storage
myFunc.cache = {};

4.2自定义模式

var scareMe = function () {
    alert("Boo!");
    scareMe = function () {
        alert("Double boo!");
    };
};

scareMe(); // Boo!
scareMe(); // Double boo!

也叫“惰性函数定义”(lazy funciton definition),因为该函数直到第一次使用时才被正确定义,而且其具有后向惰性,执行了更少的工作。

使用场景:当你的函数有一些初始化准备工作要做,并且仅需执行一次,自定义函数(self-defining funciton)可以更新自身的实现。

优点:
可提升程序性能,因为重新定义的函数仅执行更少的工作。

缺点:
1、重新定义自身时,已添加到原始函数的任何属性都会丢失;
2、如果函数使用了不同的名称(比如分配给不同的变量,或者以对象的方法来使用),那么重定义部分将永远不会发生,并且将会执行原始函数体。

var scareMe = function () {
    alert("Boo!");
    scareMe = function () {
        alert("Double boo!");
    };
};


//添加一个新的属性
scareMe.property = "properly";


// 赋值给一个不同的变量
var prank = scareMe;


// 作为一个方法使用
var spooky = {
    boo : scareMe
}


prank(); // Boo!
prank(); // Boo!
console.log(prank.property);//properly


spooky.boo(); // Boo!
spooky.boo(); // Boo!
console.log(spooky.boo.property);//properly


scareMe(); // Boo!
scareMe(); // Double boo!
console.log(scareMe.property);//undefined

注意以下情况:

var scareMe = function () {
    alert("Boo!");
    scareMe = function () {
        alert("Double boo!");
    };
};


//添加一个新的属性
scareMe.property = "properly";


// 赋值给一个不同的变量
var prank = scareMe;
prank(); // Boo!
prank(); // Boo!
console.log(prank.property);//properly


// 作为一个方法使用
var spooky = {
    boo : scareMe  //这个时候的scareMe经过上面prank()执行过后,是自身的更新函数了
}

spooky.boo(); // Double boo!
spooky.boo(); // Double boo!
console.log(spooky.boo.property);//undefined


scareMe(); // Double boo!
scareMe(); // Double boo!
console.log(scareMe.property);//undefined