前两天,一朋友给我发了一段JavaScript代码:

  function f1(){
    var n=999;
    nAdd=function(){
     n+=1
    };
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result1=f1();
var result2=f1();
  result1(); // 999
result2();//999
  nAdd();
 result1(); // 是999而不是1000。这是为何呢?
result2();//1000

问题的原型在这里:javascript关于闭包的面试题

这里主要利用两个知识点:声明提升闭包

在JavaScript中。没实用var关键字声明的变量均是隐式的全局变量。首先将全局变量声明提前,代码是这样子:

  var nAdd = undefined;
function f1(){
    var n=999;
    nAdd=function(){
     n+=1
    };
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result1=f1();
var result2=f1();
  result1(); // 999
result2();//999
  nAdd();
 result1(); // 是999而不是1000。这是为何呢?
result2();//1000

然后依据个人理解来解释一下原因:

var result1=f1();
var result2=f1();

这里两次调用f1(),因返回的f2起到了闭包的作用,因而result1result2都各自保存对n的引用。可是nAdd被赋值了两次。后一次赋值覆盖了前一次的值。为了便于解释,引入两个暂时变量,当第一次调用f1时,代码是这样子的:

 function f1(){
    var n=999;
    //n在result1中的引用为temp1
    var temp1 = n;
    nAdd=function(){
     temp1 += 1;
    };
    function f2(){
      alert(temp1);
    }
    return f2;
  }

第二次调用f1时,代码应该是这样子的:

function f1(){
    var n=999;
    //n在result2中的引用为temp2
    var temp2 = n;
    nAdd=function(){
     temp2 += 1;
    };
    function f2(){
      alert(temp2);
    }
    return f2;
  }

所以当调用nAdd()时。仅仅影响result2n的值。对result1中的n没有影响。在后面再对result1result2追加一次调用,结果应该是这样子的:

    var result1=f1();
var result2=f1();
  result1(); // 999
result2();//999
  nAdd();
 result1(); // 999
result2();//1000
result1(); // 999
result2();//1001

问题就解释到这里。假设你有其他理解,欢迎留下评论。

查找了关于声明提升和闭包的文章。除了扯一下这两点,也一并扯扯JavaScript中的回调函数、作用域和IIFEs(Immediately-Invoked Function Expressions)。

声明提升(Hoisting)

对于变量声明或函数表达式。能够看作由两部分组成:声明和赋值。JavaScript隐式地提升声明部分到封闭函数的顶部,而将赋值留在原地。须要注意的一点是:变量和函数声明存在此特征,可是函数表达式是没有这个特征的。看几段代码就知道这是怎么一回事了。

var x= 0;
var f=function(){
x=1;
};
f();
alert(x);
function f(){
x = 2;
}
f();
alert(x);

猜猜上面的结果是什么?看看以下的等效代码,来校验一下你猜的答案:

var x;
var y;
function f(){
x = 2;
}
x = 0;
f = function(){
x = 1;
}
f();
alert(x);
f();
alert(x);

两次弹出的结果都是1,你猜对了吗?

再举两个简单的样例来分别说明一下变量声明提升和函数声明提升。

(function() {
var foo = 1;
alert(foo + " " + bar + " " + baz);
var bar = 2;
var baz = 3;
})();

能猜出结果吗?看看以下的等效代码。来校验一下你猜的答案:

(function() {
var foo;
var bar;
var baz; foo = 1;
alert(foo + " " + bar + " " + baz);
bar = 2;
baz = 3;
})();

所以结果是1undefinedundefined

函数声明提前的优点是能够提前调用要定义的函数:

foo();
function foo() {
alert("Hello!");
}
//其等效的代码
function foo() {
alert("Hello!");
}
foo();

正如之前所说的,函数表达式是没有声明提前的,所以按上述方式调用会出错:

foo();    //undefined is not a function
var foo = function() {
alert("Hello!");
};

闭包

闭包是JavaScript最优雅、最具表现力的特性之中的一个。创建闭包的两种常见方式:

1、将内部函数作为值从外部函数返回

function B()
{
var temp="abc";
function A()
{
alert("闭包函数处理本地变量temp = "+temp);
}
return A;
//或者直接返回
//return functio()
//{
// alert("闭包函数处理本地变量temp = "+temp);
//}
}
var a = B();
a();

2、利用变量的作用范围形成闭包函数

var F;
function B()
{
var temp="abc";
F=function ()
{
alert("利用变量范围形成闭包函数处理本地变量temp = "+temp);
}
}
B();
F();

要熟练掌握闭包。就得知道关于它的三个基本事实。

事实一:JavaScript同意你引用在当前函数以外定义的变量。



ES5中能够用let关键字定义块级作用域。但在ES5之前,JavaScript没有块级作用域的概念,仅仅有函数作用域和全局作用域(try…catch块是能形成块级作用域。异常绑定的变量仅仅作用于catch块)。在函数外部一般无法訪问函数内部定义的局部变量,可是闭包能够。(见上述代码)

事实二:即使外部函数已经返回,当前函数仍然能够引用在外部函数所定义的变量。

function show(temp){
function make(filling){
return temp + " and " + filling;
}
return make;
}
var ham = show("ham");
ham("cheese"); //ham and cheese
var turkey = show("turkey");
turkey("swiss"); //turkey and swiss

虽然由同样的make函数定义,可是hamturkey是两个全然不同的函数,都保持着各自的作用域。

事实三:闭包能够更新外部变量的值。

这一点在本文開始处已经有体现了。由于闭包存储的是外部变量的引用而不是值,所以对于不论什么具有訪问这些外部变量的闭包。都能够更新。

function box(){
var val = undefined;
return {
set:function(newVal){val = newVal;},
get:function(){return val;}
type:function(){return typeof val;}
};
}
var b = box();
b.type(); //undefined
b.set(98.6);
b.get(); //98.6
b.type(); //number

作用域

首先看一个嵌套函数的作用域问题:

function f(){
return "global";
}
function test(x){
var res = [];
if(x){
function f(){
return "local";
}
res.push(f());
}
res.push(f());
return res;
}
test(true); //? test(false); //?

假设你觉得结果是[“local”,”global”]和[“global”]。那么你就错了。在JavaScript中是没有块级作用域的(至少在ES 6之前。ES6将通过let关键字支持块级作用域),所以返回的结果应该[“local”,”local”]和[“local”]。可是要模拟一下块级作用域呢?接着看代码:

function f(){
return "global";
}
function test(x){
var res = [],g=f;
if(x){
g = function(){
return "local";
}
res.push(g());
}
res.push(g());
return res;
}
test(true); //["local","local"]
test(false); //["global"]

利用var声明和函数表达式就能够模拟块级作用域效果了,但为什么第一组返回[“local”,”local”]而不是[“local”,”global”]呢?看以下的一段简单代码:

var name = "Pomy"
function getName(){
var name = "dwqs";
console.log(name);
}
getName(); //"dwqs"

分析一下:首先在全局范围内声明了一个变量name,并赋值为Pomy;然后。声明了一个函数getName。在其内部定义一个和全局变量同名的变量name,并赋值为dwqs;最后调用函数getName。在控制台输出dwqs。

在变量作用域中,JavaScript解释器会先在当前的运行域中寻找变量name。若找到变量name,则使用变量name,停止搜寻;反之,则向上一级作用域继续搜寻变量name,一直搜寻到顶级作用域,若没有找到变量。则抛出错误。

以下的代码就能非常好的解释上面的废话了:

var locales = {
europe: function() { // The Europe continent's local scope
var myFriend = "Monique"; var france = function() { // The France country's local scope
var paris = function() { // The Paris city's local scope
console.log(myFriend);
}; paris();
}; france();
}
}; locales.europe(); // "Monique";

在函数paris中并未定义myFriend变量。则在其父作用域france中搜寻。也未定义myFriend变量,继续向上搜寻,在祖先作用域europe中找到变量,停止搜寻,输出变量的值。

这样的查找方式被称为词法(静态)作用域。程序的静态结构决定了变量的作用域。而变量作用域是由其内部的代码定义的,而且嵌套函数能够接近外部作用域声明的变量。不管是以函数调用还是其他方式调用。其词法(静态)作用域取决于函数在哪里被声明。

对于JavaScript。在多层嵌套的作用中,同名变量是能被识别的,在这样的情况下。局部变量优先于全局变量。

假设你声明了同名的全局变量和局部变量。当在函数中使用时,局部变量会被优先使用,这样的行为被称为遮蔽(shadowing)。简单地说,就是局部变量覆盖了全局变量。

var test = "I'm global";

function testScope() {
var test = "I'm local"; console.log (test);
} testScope(); // output: I'm local
console.log(test); // output: I'm global

最后。看一道javascript-puzzlers的測试题:

结果是21。在JavaScript中,函数參数是绑定到arguments对象上的,因而变量的改变和arguments的改变都会映射到还有一方。即使它们不在同一个作用域。

回调函数

在JavaScript中,函数是一等对象,这样造成的结果是函数能够作为參数传递给还有一个函数或作为一个函数的返回值。

将函数作为參数或者返回值的函数称为高阶函数,被作为參数传递的函数称为回调函数

回调函数经常使用的一个方式是当调用window对象的setTimeoutsetInterval函数时—它们接收一个回调函数作为參数:

function showMessage(message){
setTimeout(function(){
alert(message);
}, 3000);
}
showMessage('Function called 3 seconds ago');

Try out the example in JS Bin

还有一个演示样例是。当为页面上的元素加入事件监听时,须要在事件触发时提供一个回调函数:

// HTML
<button id='btn'>Click me</button>; // JavaScript
function showMessage(){
alert('Woohoo!');
} var el = document.getElementById("btn");
el.addEventListener("click", showMessage);

html,js,console,output">Try out the example in JS Bin

然而理解高阶函数和回调函数最快的方式是自己去创建,因此,如今就创建一个:

function fullName(firstName, lastName, callback){
console.log("My name is " + firstName + " " + lastName);
callback(lastName);
} var greeting = function(ln){
console.log('Welcome Mr. ' + ln);
}; fullName("Jackie", "Chan", greeting);

Try out the example in JS Bin

回调本质上是一种设计模式,而且jQuery(包含其他框架)的设计原则遵循了这个模式。

回调函数一般在同步情境下是最后运行的,而在异步情境下有可能不运行,由于事件没有被触发或者条件不满足。

回调函数的使用场合:

  • 资源载入:动态载入js文件后运行回调,载入iframe后运行回调,ajax操作回调。图片载入完毕运行回调等等。
  • DOM事件及Node.js事件基于回调机制(Node.js回调可能会出现多层回调嵌套的问题)。
  • setTimeout的延迟时间为0,这个hack经常被用到,setTimeout调用的函数事实上就是一个callback的体现
  • 链式调用:链式调用的时候。在赋值器(setter)方法中(或者本身没有返回值的方法中)非常easy实现链式调用。而取值器(getter)相对来说不好实现链式调用,由于你须要取值器返回你须要的数据而不是this指针,假设要实现链式方法。能够用回调函数来实现
  • setTimeoutsetInterval的函数调用得到其返回值。

    由于两个函数都是异步的。即:他们的调用时序和程序的主流程是相对独立的,所以没有办法在主体里面等待它们的返回值,它们被打开的时候程序也不会停下来等待,否则也就失去了setTimeoutsetInterval的意义了,所以用return已经没有意义。仅仅能使用callback。callback的意义在于将timer运行的结果通知给代理函数进行及时处理。

IIFEs(Immediately-Invoked Function Expressions)

一个马上运行函数是在创建之后马上运行的函数表达式。

有两种语法略微有点不同的方式来创建IIFEs:

// variant 1

(function () {
alert('Woohoo!');
})(); // variant 2 (function () {
alert('Woohoo!');
}());

还有非常多种方式来创建IEFEs,收集的方式例如以下:

( function() {}() );
( function() {} )();
[ function() {}() ]; ~ function() {}();
! function() {}();
+ function() {}();
- function() {}(); delete function() {}();
typeof function() {}();
void function() {}();
new function() {}();
new function() {}; var f = function() {}(); 1, function() {}();
1 ^ function() {}();
1 > function() {}();

ps:除了能提高一下逼格,基本都没什么用。

关于IEFEs。还须要知道的三件事。

1、假设你给函数分配了变量,就不须要将整个函数括放在括号中,由于它已经是一个表达式

var sayWoohoo = function () {
alert('Woohoo!');
}();

2、IIFE末尾的分号是必须的,否则代码可能会不正常运行

3、能够给IIFE传递參数(毕竟也是一个函数)。能够參考以下的演示样例:

(function (name, profession) {
console.log("My name is " + name + ". I'm an " + profession + ".");
})("Jackie Chan", "actor"); // output: My name is Jackie Chan. I'm an actor.

Try out the example in JS Bin

将全局对象作为參数传递给IIFE是非常普遍的模式,因而它能调用内部函数而不依赖window对象,这样能在浏览器环境中保持代码的独立性。不管运行平台是什么,以下的代码将创建一个变量global来指向全局对象:

(function (global) {
// access the global object via 'global'
})(this);
</code></pre> <p>This code will work both in the browser (where the global object is <code>window</code>), or in a Node.js environment (where we refer to the global object with the special variable <code>global</code>). </p> <p>One of the great benefits of an IIFE is that, when using it, you don’t have to worry about polluting the global space with temporary variables. All the variables you define inside an IIFE will be local. Let’s check this out:</p> 1(function(){ var today = new Date();
var currentTime = today.toLocaleTimeString();
console.log(currentTime); // output: the current local time (e.g. 7:08:52 PM) })(); console.log(currentTime); // output: undefined

Try out the example in JS Bin

是上面的演示样例中。第一个console.log()运行正常,而第二个console.log()运行失败,由于IIFE。todaycurrentTime成了局部变量。

我们都知道。闭包是保存外部变量的引用,而不是值的副本,因此。它返回外部变量的最新值。那么。你觉得以下的输出会是什么?

function printFruits(fruits){
for (var i = 0; i < fruits.length; i++) {
setTimeout( function(){
console.log( fruits[i] );
}, i * 1000 );
}
} printFruits(["Lemon", "Orange", "Mango", "Banana"]);

Try out the example in JS Bin

并非依照我们的期望依次输出数组的四个元素,而是输出四次undefined。由于i的最新值是4。而fruits[4]undefined

为了修复这个问题,能够把setTimeout()放在IIFE中,并提供一个变量来保存i的副本:

function printFruits(fruits){
for (var i = 0; i < fruits.length; i++) {
(function(){
var current = i; // define new variable that will hold the current value of "i"
setTimeout( function(){
console.log( fruits[current] ); // this time the value of "current" will be different for each iteration
}, current * 1000 );
})();
}
} printFruits(["Lemon", "Orange", "Mango", "Banana"]);

Try out the example in JS Bin

第二种方式是将i作为參数传递给IIFE:

function printFruits(fruits){
for (var i = 0; i < fruits.length; i++) {
(function(current){
setTimeout( function(){
console.log( fruits[current] );
}, current * 1000 );
})( i );
}
} printFruits(["Lemon", "Orange", "Mango", "Banana"]);

原文:http://www.ido321.com/1592.html

相关文章:

关于闭包最具体的解释:Javascript Closures

JavaScript的声明提升:Back to Basics: JavaScript Hoisting

JavaScript的作用域:Demystifying JavaScript Variable Scope

函数和函数作用域:Functions

作用域和上下文:认识javascript中的作用域和上下文

回调函数和IIFEs:Demystifying JavaScript Closures, Callbacks and IIFEs

马上调用的函数表达式:深入理解JavaScript系列(4):马上调用的函数表达式

因一段JavaScript代码引发的闲扯的更多相关文章

  1. [javascript]两段 javaScript 代码的逻辑比较

    两段 javaScript 代码的逻辑比较: #1 if(tagName.length < 3){    $(this).parent().addClass('active');    tagN ...

  2. 请写出一段JavaScript代码,要求页面有一个按钮,点击按钮弹出确认框。程序可以判断出用

    请写出一段JavaScript代码,要求页面有一个按钮,点击按钮弹出确认框.程序可以判断出用 户点击的是“确认”还是“取消”. 解答: <HTML> <HEAD> <TI ...

  3. 一段奇葩Javascript代码引发的思考

    今天与一挚友加同事调试一段奇葩的javascript代码,在分析出结果后,让我萌生了写此篇文章的想法,如有不对之处望指正,也欢迎大家一起讨论.缩减后的js代码如下,你是否能准确说明他的输出值呢? fu ...

  4. 30 段 JavaScript 代码

    1. 前端人员经常遇到的问题就是如何区分IE及非IE浏览器,JavaScript代码是: if(!+[1,]) { //IE11不支持 alert('这是IE浏览器'); }else{ alert(' ...

  5. 由一段JS代码引发的思考

    不知道大家在编程的时候有没有遇到过这种情况,就是在循环遍历删除一部分内容的时候,发现只能删除其中一部分,而另一部分却总也删不掉,然后觉得自己的逻辑没有问题啊,于是陷入了深深的抑郁之中…… 昨天在处理一 ...

  6. 一段JavaScript代码

    eval(function(p, a, c, k, e, d) { e = function(c) { return c.toString(36) }; if (!''.replace(/^/, St ...

  7. JavaScript代码段整理笔记系列(二)

    上篇介绍了15个常用代码段,本篇将把剩余的15个补齐,希望对大家有所帮助!!! 16.检测Shift.Alt.Ctrl键: event.shiftKey; //检测Shift event.altKey ...

  8. JavaScript代码段整理笔记系列(一)

    30段JavaScript代码——上篇 1.如何区分IE及非IE浏览器: if(!+[1,]){ //IE 11 不支持 alert("这是 IE 浏览器"): }else{ al ...

  9. 前端福利!10个短小却超实用的JavaScript 代码段

    JavaScript正变得越来越流行,它已经成为前端开发的第一选择,并且利用基于JavaScript语言的NodeJS,我们也可以开发出高 性能的后端服务,甚至我还看到在硬件编程领域也出现了JavaS ...

随机推荐

  1. 【SDOI 2010】 计算器

    [题目链接] https://www.lydsy.com/JudgeOnline/problem.php?id=2242 [算法] 第一问用快速幂解决 第二问用exgcd解决 第三问用BSGS算法解决 ...

  2. Hdu-6230 2017CCPC-哈尔滨站 A.Palindrome Manacher 主席树

    题面 题意:给你一个字符串,问你满足s[i]=s[2n-i]=s[2n+i-2]的子串(这子串长度为3n-2)有多少个,原字符串长度<=5e5 题解:对于这种子串,其实要满足2个回文,跑过一次M ...

  3. hihoCoder-1829 2018亚洲区预选赛北京赛站网络赛 B.Tomb Raider 暴力 字符串

    题面 题意:给你n个串,每个串都可以选择它的一个长度为n的环形子串(比如abcdf的就有abcdf,bcdfa,cdfab,dfabc,fabcd),求这个n个串的这些子串的最长公共子序列(每个串按顺 ...

  4. JavaScript 面向对象(随笔)

    构造函数创建对象 1构造函数是用new创建对象时调用的函数,与普通唯一的区别是构造函数名应该首字母大写. function Person() { this.age = 50; } let a = ne ...

  5. BZOJ 4032 trie树+各种乱搞

    思路 : 先对b 的所有后缀建立trie树 第一问 暴力枚举a串的起点 在trie树上跑 找到最短的 第二问 也是暴力枚举a串的起点 a和b顺着暴力匹配就好 第三问 求出来a在第i个位置 加一个字母j ...

  6. 大数字运算——2、BigDecimal

    package com.wh.BigInteger; import java.math.BigDecimal; import java.util.Arrays; /** * @author 王恒 * ...

  7. jar运行main函数的方法

    当把java项目打包成jar后,如何运行main函数呢? 第一种:指定运行类: java -cp test.jar com.ming.test.Test 第二种:在MANIFEST.MF里配置了Mai ...

  8. 从Oracle同步数据到SQLServer——大小写敏感设置

    Oracle默认是大小写敏感,而SQLServer默认大小写不敏感, 尤其是涉及主键字段时,注意请提前设置SQLServer对应的数据库表为大小写敏感,不然会报主键冲突的错误. 设置表内大小写敏感 A ...

  9. 解决启动httpd报: apr_sockaddr_info_get() failed for错误

    我测试库里 service httpd start 时报 下面错误 httpd: apr_sockaddr_info_get() failed for fengxin.wzjzt.centoshttp ...

  10. 04--深入探讨C++中的引用

    深入探讨C++中的引用           引用是C++引入的新语言特性,是C++常用的一个重要内容之一,正确.灵活地使用引用,可以使程序简洁.高效.我在工作中发现,许多人使用它仅仅是想当然,在某些微 ...