JS学习6(函数表达式)

定义函数的方法有两种:函数声明和函数表达式。

函数声明

使用函数声明时,函数声明会被提升至当前作用域最前面。

//这样也不会报错
sayHi();
function sayHi(){
    alert("Hi!");
}

但是这个特性也就造成了这样的使用是不可预测的:

if(condition){
    function sayHi(){
        alert("Hi!");
    }
} else {
    function sayHi(){
        alert("Yo!");
    } 
}

这段代码本来的目的是在两个if的情况下使用不同的函数定义,但是在函数声明被提前的情况下,这显然是做不到的。所以在JS里永远也不要这么做。

函数表达式

函数表达式就是将一个匿名函数赋值给一个变量,所以直到执行到这句代码之前,这个函数都是不存在的。

sayHi(); // 报错函数不存在
var sayHi = function(){
    alert("Hi!");
};

不过这样的特性就可以实现刚才的目的咯~

var sayHi;
if(condition){
    sayHi = function(){
        alert("Hi!");
    };
} else {
    sayHi = function(){
        alert("Yo!");
    };
}

递归

就是函数自己调用自己咯

最初级的版本

function factorial(num){
    if (num <= 1){
        return 1;
    } else {
        return num * factorial(num-1);
    }
}

这个看起来并没有什么问题呀,在函数定义里调用了自己。但是由于有函数表达式的存在,这样就会出错了:

//将anotherFactorial指向函数
var anotherFactorial = factorial;
//将factorial指向空
factorial = null;
//再调用这个函数时,里面的factorial(num-1)就无法执行了
alert(anotherFactorial(4)); 

第二个版本

使用arguments.callee这个指向正在执行函数的指针可以解决这个问题。

function factorial(num){
    if (num <= 1){
        return 1;
    } else {
        return num * arguments.callee(num-1);
    } 
}

这样就解决了函数名在递归函数里被写死的问题。但是在严格模式下访问不到这个指针哦,所以。。。。还得想个办法。

第三个版本

这时函数表达式就上场了。

var factorial = (function f(num){
    if (num <= 1){
        return 1;
    } else {
        return num * f(num-1);
    } 
});

这样写即便把函数再赋值给别的变量f()还是可以保留。其实这个和第一种方法的本质是一样的,而且有个大问题就是这个函数表达式要放在调用前面。

闭包

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式就是在一个函数中创建另一个函数。就像这样:

function createComparisonFunction(propertyName) {
    return function(object1, object2){
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];
        if (value1 < value2){
            return -1;
        } else if (value1 > value2){
            return 1;
        } else {
            return 0;
        } 
    };
}
var compare = createComparisonFunction("name");
var result = compare({ name: "Nicholas" }, { name: "Greg" });

在内部定义的匿名函数中,访问了外部函数中的变量propertyName。即使这个匿名函数被返回赋值给另一个变量,在其他地方通过这个变量调用了这个匿名函数,它仍然可以访问propertyName这个变量。这是因为这个匿名函数的作用域链里包含着createComparisonFunction()的作用域。

普通函数的作用域链

unction compare(value1, value2){
    if (value1 < value2){
        return -1;
    } else if (value1 > value2){
        return 1;
    } else {
        return 0; 
    }
}
var result = compare(5, 10);

在这里我们先定义了compare()函数,然后又在全局作用域中调用了它。
当某个函数被调用时,会创建一个执行环境及相应的作用域链。每一个执行环境都有一个表示变量的对象(变量对象),全局的变量对象始终存在,而像函数这样的局部环境的变量对象则只存在于函数执行的过程中。
在创建compare()时,会预先创建一个包含全局变量对象的的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。
当调用compare()时,函数的执行环境就被创建了,并通过复制函数[[Scope]]的对象构建起该执行环境的作用域链。接下来会创建这个函数的活动对象,这个对象包括arguments和其他命名参数,活动对象在此作为这个局部执行环境的变量对象被推入作用域链的最前端。作用域链本质上就是一个指向变量对象的指针列表。


这里写图片描述

在函数中访问一个变量时,就会在作用域链中搜索具有相应名字的变量。对于一般的函数,在执行完毕后活动对象(局部变量对象)就会被销毁。内存中仅保留全局执行环境的变量对象。

闭包的作用域链

在一个函数内部定义的函数会将外部函数的活动对象添加到它的作用域链中。
以上面的例子来说

var compare = createComparisonFunction("name");
var result = compare({ name: "Nicholas" }, { name: "Greg" });

当匿名函数从createComparisonFunction()返回到compare变量中时同样要创建一个作用域链,这个作用域链里不仅包含全局执行环境的变量对象,还包含createComparisonFunction()的活动对象。
这就意味着匿名函数可以访问createComparisonFunction()中定义的所有变量。且在createComparisonFunction()执行完之后,其执行环境的作用域链被销毁了,但是其活动对象并不像往常一样会被销毁,而是还留存在内存中。因为这时匿名函数的作用域链仍在引用这个对象。所以直到匿名函数被销毁,createComparisonFunction()的活动对象才会被消灭。

compareNames = null;
这里写图片描述

闭包与变量

闭包所保存的是外层函数的整个变量对象,也就是说,闭包只能外面的变量的最终一个值。

function createFunctions(){
    var result = new Array();
    for (var i=0; i < 10; i++){
        result[i] = function(){
            return i;
        };
    }
    return result;
}
var a = createFunctions();
alert(a[1]());   //10

按理说每个函数都应该返回自己的索引数呀。但是对不起,所有匿名函数中保存的createFunctions的活动对象都是一个哦,所以i也是一个哦,最后i变为了10.所以所有匿名函数访问i时都会得到10。
你可以这样强制:

function createFunctions(){
    var result = new Array();
    for (var i=0; i < 10; i++){
        result[i] = function(num){
            return function(){
                return num; 
            };
        }(i); 
    }
    return result;
}
var a = createFunctions();
alert(a[1]());   //1

由于i到num是值传递的,所以对于每一个result[i],num是不同的。这样每次调用时访问的就是对于每一个元素都不同的num变量了。

关于this对象

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        return function(){
            return this.name;
        };
    } 
};
alert(object.getNameFunc()()); //"The Window" 

每个函数在被调用时都会自动取得两个特殊变量,this和arguments。函数在搜索这两个变量的时候只搜索到自己的活动对象为止,并不在作用域链中搜索(其实这很好想,就相当于自己的执行环境里有这两个变量,为啥还要往上找。)匿名函数最后是在全局变量中执行的,所以this指向全局环境。

var name = "The Window";
var object = {
    name : "My Object",
    getName: function(){
        return this.name;
    };
}

object.getName(); //"My Object"
(object.getName)(); //"My Object"
(object.getName = object.getName)(); //"The Window"

有时即使是细微的变化也会引起this的值的变化。这里后两个例子不太明白。

内存泄漏

这个主要是因为IE9之前对于JS对象和BOM/DOM对象采用不同的垃圾回收机制造成的。对DOM元素使用的引用计数方法无法应对循环引用的情况。

function assignHandler(){
    var element = document.getElementById("someElement");
    element.onclick = function(){
        alert(element.id);
    };
}

在这里element对匿名函数有引用,匿名函数引用着assignHandler()的活动对象,其中就包含着element,啊哦。
所以。。。。。抛弃IE吧!
好吧。。。。可以这样改。。。

function assignHandler(){
    var element = document.getElementById("someElement"); 
    var id = element.id;
    element.onclick = function(){
        alert(id);
    };
    element = null;
}

仅仅把闭包中的element移出去是木有用哒,闭包包含着外面函数的活动对象哦,element的引用还在,所以要手工减掉element对DOM的引用,对匿名函数的引用也就没了。

模仿块级作用域

对于多次声明同一个变量,JS不会阻止你这样做,而且如果你在多次的声明中执行了变量初始化,JS会照做不误。这就带来了很大的问题,你自己的代码可能比较了解,但是当你使用了框架或别人的JS时,天知道你们的变量名有没有重复的!
所以,模仿块级作用域的需求就来了。
看看是怎么演变来的

var someFunction = function(){
    //块级作用域      
};
someFunction();

我们是可以用时机的值替换变量名的

function(){ 
    //块级作用域     
}(); //报错   

这里会报错是因为JS将function视作一个函数声明的开始,函数声明后面不能跟圆括号。那么我们将它转换为函数表达式吧

(function(){
    //块级作用域    
})();

私有变量

任何在函数中定义的变量就是私有变量,因为外部访问不到。现在我们有了闭包,闭包可以被返回到作用域外面,而通过闭包又可以访问作用域里的变量和方法。这样我们就可以来模仿私有变量和公有方法了。

function MyObject(){
    var privateVariable = 10;
    function privateFunction(){
        return false;
    }
    this.publicMethod = function (){
        privateVariable++;
        return privateFunction();
    };
}

privateVariable和privateFunction()只有通过publicMethod方法才能访问。
使用这样的方法,我们可以隐藏那些不能被修改的数据。

function Person(name){
    this.getName = function(){
        return name;
};
    this.setName = function (value) {
        name = value;
}; }
var person = new Person("Nicholas");
alert(person.getName());   //"Nicholas"
person.setName("Greg");
alert(person.getName());   //"Greg"

这里如果我们只提供get方法,name就不会被改变。都不提供那name对于对象的使用者来说就是透明的。
但是在构造函数中定义特权方法有个问题,那就是你必须使用构造函数模式来达到这个目的,也就是说对于每一个实例都会创造一组新的方法。

静态私有变量

(function(){
    var name = "";
    Person = function(value){
        name = value;
    };
    Person.prototype.getName = function(){
        return name;
    };
    Person.prototype.setName = function (value){
    name = value;
};
})();

var person1 = new Person("Nicholas"); 
alert(person1.getName()); //"Nicholas" 
person1.setName("Greg"); 
alert(person1.getName()); //"Greg"

var person2 = new Person("Michael");
alert(person1.getName()); //"Michael"
alert(person2.getName()); //"Michael"

静态私有变量创建了一个私有作用域,其中Person这个变量定义时没有通过var声明,这样它就是个全局变量。于是在它原型里的方法在私有作用域外也可以访问到。但这里name属性是所有实例共享的。

模块模式

为单例创建私有变量和特权方法的模式,单例是只有一个实例的对象:

var singleton = {
    name : value,
    method : function () { 
    } 
};

模块模式为单例添加私有变量和特权方法:

var application = function(){
    var components = new Array();
    components.push(new BaseComponent());
    return {
        getComponentCount : function(){
            return components.length;
        },
        registerComponent : function(component){
            if (typeof component == "object"){
                components.push(component);
            }
        } 
    };
}();

在这里,最后返回的单例对象是在匿名函数里创建的,所以这里面的方法可以访问到匿名函数里的私有变量。外界只能通过着两个方法有限的修改components,只能获取长度和添加。但是这里的单例是Object对象。

增强的模块模式

这种模式适合要返回的单例是特定类型的,本质上和普通的模块模式没啥区别。

var application = function(){
    var components = new Array();
    components.push(new BaseComponent());
    
    var app = new BaseComponent();
    app.getComponentCount = function(){
        return components.length;
    };
    app.registerComponent = function(component){
        if (typeof component == "object"){
            components.push(component);
        }
    };     
    return app;
}();

这样app对象就满足了必须是某个类型的单例的要求。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容

  • 定义函数的方式有两种:函数声明和函数表达式。 函数声明的一个重要特征就是函数声明提升,意思是在执行代码前会先读取函...
    oWSQo阅读 608评论 0 0
  • 本章内容 函数表达式的特征 使用函数实现递归 使用闭包定义私有变量 定义函数的方式有两种:一种是函数声明,另一种就...
    闷油瓶小张阅读 340评论 0 0
  • 最近学这块知识学得有些吃力。还有很多遗漏的地方,只能以后多看些书来弥补了。 第7章 函数表达式 函数定义的两种方式...
    丨ouo丨阅读 340评论 0 1
  • 一早醒来,发现周围不再是熟悉的场景。首先看到的,也不再是天花板,而是太阳初升粉红橙色的朝霞满天,有多久没有见到这么...
    繁花坞阅读 6,458评论 10 16
  • 词:董书利你是一本书未轻易去翻那不加修饰的封面足以让我思绪万千 你宁愿是迷我无力承受失去也不想历史重演来祭奠自己的...
    星巢文化阅读 285评论 4 4