StackOverflow 上那些 JavaScript 的高赞问题

1,234 阅读9分钟

问不出好问题,又想看到好答案。怎么办,只好做个答案的搬运工。以下问题来自于 StackOverflow 中 JavaScript 话题下的高票回答。我们来一睹为快。

1.如何移除数组中的特殊元素?

答(1)

11125 赞

使用 indexOf 找出你想移除元素的 index,然后使用 splice 移除元素。

splice() 方法在移除存在元素或者添加新元素的时候,会改变当前数组内容。(译注:即该函数具有副作用。)

const array = [2, 5, 9];

console.log(array);

const index = array.indexOf(5);
if (index > -1) {
  array.splice(index, 1);
}

// array = [2, 9]
console.log(array);

splice 的第二个参数是移除元素的个数。注意这里,splice 修改了原来数组的位置,然后返回一个包含移除元素的新数组。

2.如何重定向到另一个页面?

答(2)

14467 赞

一个不是简单使用 jQuery 重定向的方法

jQuery 不是必须的,window.location.replace(...) 是模拟 HTTP 重定向的最好方法。

window.location.replace(...)window.location.href 更好,因为 replace() 没有在会话历史中保留原始页面,意味着没有回退按钮给用户使用。

如果想模拟用户点击链接,使用 location.href

如果想模拟 HTTP 重定向,使用 location.replace

3.JavaScript的闭包如何工作?

答(3)

7152 赞

闭包不是魔法

这篇回答解释了闭包在 JavaScript 如何使用,为了让程序员更容易理解,本文不面向专家和在函数式编程上有深厚经验的程序员。

闭包从核心概念上来说并不难理解。然后从学术或者理论解释上来说,又很难明白!

这篇文章适用于那些在主流程序语言中有些编程经验的的程序员,他们可以读到过以下代码:

function sayHello(name) {
  var text = 'Hello ' + name;
  var say = function() { console.log(text); }
  say();
}
sayHello('Joe');

两点简单总结

  • 当函数(foo)声明了另一些函数(bar 和 baz),在 foo 中创建的局部变量在函数存在的时候不会摧毁。变量只是在外部世界不可见。foo 可以借此机会巧妙地返回函数 barbza,然后它们可以继续读写以及通过闭包相互交流,在闭包中没有人可以干扰到它们,即使有人之后再次调用了 foo
  • 闭包是支持函数是一等公民的证明。闭包是一个引用变量的表达式,这个变量在声明的时候包含着自己的作用域,可以被赋值给其他变量,可以当做参数传递给函数,或者当做结果从函数中返回。

一个闭包的例子

以下代码返回一个函数引用:

function sayHello2(name) {
  var text = 'Hello ' + name; // Local variable
  var say = function() { console.log(text); }
  return say;
}
var say2 = sayHello2('Bob');
say2(); // logs "Hello Bob"

上面的代码中,函数的引用返回给了变量 say2。C 语言程序员可能认为函数返回了一个指针给另一个函数,同时 saysay2 各是一个函数的指针。

在 C 语言的指针和 JavaScript 引用函数之间有个至关重要的不同。你可以认为函数引用的变量既是指向函数的指针也是指向闭包的隐藏指针。

上面的代码有一个闭包。因为匿名函数 function() {console.log(test)} 在另一个函数体 sayHello() 中声明。在 JavaScript 中,如果你使用 function 关键字在另一个函数中,那么你就创建了一个闭包。

在 C 和绝大多数语言中,当函数返回之后,因为调用栈被销毁,所有的局部变量不再可以访问。

在 JavaScript 中,在函数体内声明另一个函数,那么在函数返回后,外部函数的局部变量仍然可以被访问。上面的例子已经证明,我们在函数 sayHello2 返回后调用函数 say2。注意我们可以引用的变量 text,它是函数 sayHello2() 的局部变量。

function() { console.log(text); } // say2.toString() 的输出;

观察 say2.toString() 的输出,我们可以看到代码引用的变量 text。这个匿名函数可以引用 text 保持值是 Hello Bob。因为 sayHello2() 的局部变量秘密地在闭包中存在。

这个特性,即在 JavaScript 中的函数引用也同样有一个秘密的引用指向里面创建的闭包。类似如何委托称为方法指针秘密地引用一个对象。

更多例子

由于某些原因,闭包看起来很难理解,但如果你看了一些例子之后,它会变得清晰起来。我推荐大家手动写一下下面的例子,直到理解它们如何工作的时候在真正使用他们。如果你没有完全理解闭包就去使用的话,可能会导致一些奇怪的 bug!

例子 3

这个例子展示了局部变量不可复制 —— 它们通过引用被保留下来。就好像调用栈在内存中存在一样,甚至在外部函数消失之后仍然一样!

function say667() {
  // Local variable that ends up within closure
  var num = 42;
  var say = function() { console.log(num); }
  num++;
  return say;
}
var sayNumber = say667();
sayNumber(); // logs 43

例子 4

下面三个全局函数有着共同的闭包,因为它们都是在一次调用的 setupSomeGlobals() 中生成。

var gLogNumber, gIncreaseNumber, gSetNumber;
function setupSomeGlobals() {
  // 局部变量存储在闭包中
  var num = 42;
  // 存储函数引用作为全局变量
  gLogNumber = function() { console.log(num); }
  gIncreaseNumber = function() { num++; }
  gSetNumber = function(x) { num = x; }
}

setupSomeGlobals();
gIncreaseNumber();
gLogNumber(); // 43
gSetNumber(5);
gLogNumber(); // 5

var oldLog = gLogNumber;

setupSomeGlobals();
gLogNumber(); // 42

oldLog() // 5

当三个函数被定义时,上面三个函数共享同一个闭包 —— 也就是 setupSomeGlobals() 的局部变量。

注意上面的例子,如果你再次调用 setupSomeGlobals(),那么新的闭包(调用栈)会被建立。

旧的变量 gLogNumber, gIncreaseNumber, gSetNumber 会被有新闭包的新函数覆盖。在 JavaScript 中,当你再其他函数内部声明了函数,每当外部函数被调用时,内部函数总是会重新创建。

例子 5

这个例子展示了在外部函数消失前闭包包含的任何声明在内部的局部变量。注意变量 alice 实际上声明在匿名函数之后。匿名函数首先声明,然后函数被调用的时候可以访问 alice 变量,因为 alice 在同一个作用域(JavaScript 存在变量提升)。同样的,sayAlice()() 只是直接调用了函数引用然后从 sayAlice() 返回 —— 其实跟之前做的一样,只是没有了临时变量。

function sayAlice() {
    var say = function() { console.log(alice); }
    var alice = 'Hello Alice';
    return say;
}
sayAlice()();// logs "Hello Alice"

注意, say 变量也在闭包里面,同时可以被其他在 sayAlice() 中声明的函数访问,或者被内部函数递归调用。

例子 6

有个对每个人来说都需要知道的事,更需要理解。如果在循环中定义方法的话,一定要格外小心:闭包中的局部变量可能不会按照你的设想表现。

需要理解变量提升在 JavaScript 中的特点,看看下面的例子。

function buildList(list) {
    var result = [];
    for (var i = 0; i < list.length; i++) {
        var item = 'item' + i;
        result.push( function() {console.log(item + ' ' + list[i])} );
    }
    return result;
}

function testList() {
    var fnlist = buildList([1,2,3]);
    for (var j = 0; j < fnlist.length; j++) {
        fnlist[j]();
    }
}

 testList() //输出 "item2 undefined" 3 次

这行 result.push( function() {console.log(item + ' ' + list[i])} 代码给返回的数组添加了三次匿名函数引用。如果不是很熟悉匿名函数的话,可以把它当做如下:

pointer = function() {console.log(item + ' ' + list[i])};
result.push(pointer);

运行上面的例子,会输出三次 item2 undefined。如同之前的例子一样,buildList 的局部变量只有一个闭包,这些变量是 result, i, listitem。当匿名函数在 fnlist[j]() 中调用时,他们共享一个闭包,同时这些值 iitem 都在这个闭包中(在此处,i 的值是 3,item 的值是 item2 ,因为循环到这里已经执行完毕了)。注意索引是从 0 开始的,因此 item 的值是 item2,i++ 的值是 3

需要注意的是,let 不存在变量提升,而 var 不一样。如下:

使用 let 输出会变成:

item0 1
item1 2
item2 3

下图也说明了这个问题:

![](/blog/images/stk1.jpg)

例子 7

在这最后一个例子中,主函数的每次调用都会创建一个单独的闭包:

function newClosure(someNum, someRef) {
    // Local variables that end up within closure
    var num = someNum;
    var anArray = [1,2,3];
    var ref = someRef;
    return function(x) {
        num += x;
        anArray.push(num);
        console.log('num: ' + num +
            '; anArray: ' + anArray.toString() +
            '; ref.someVar: ' + ref.someVar + ';');
      }
}
obj = {someVar: 4};
fn1 = newClosure(4, obj);
fn2 = newClosure(5, obj); // 注意这里给新变量赋值一个新闭包
fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4;
fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4;
obj.someVar++;
fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5;
fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;

总结

上面的例子最好的方式是去做着试试看。读这些例子比理解它们要困难的多。上面的解释中关于闭包和调用栈的内容从技术上说并不完全正确,只是为了简单理解。 在最后,我们梳理下一下内容:

  • 任何时候只要你在函数内部创建了另一个函数,你就是用了闭包
  • 任何时候只要在函数内部使用了 eval ,那么就使用了闭包。eval 字段参考了函数的局部变量,在 eval 中,你可以创建新的局部变量
  • 当在函数内部使用 new Function(...) 时,不会创建新的闭包(当然也就不能引用外部函数的局部变量)
  • JavaScript 中的闭包像是拷贝了一份所有的局部变量并持续存在,这只在函数存在的时候是这样
  • 最好考虑总是在函数的入口创建闭包,并保存所有的局部变量
  • 每次调用带有闭包的函数时,都会保留一组新的局部变量(假设该函数内部包含函数声明,并且将返回对该内部函数的引用或以某种方式为其保留外部引用 )
  • 两个函数或许源码相同,但是行为完全不同,因为它们有隐藏的闭包。我觉得 JavaScript 不能真正找出一个函数是否引用了一个闭包
  • 尝试去动态修改源码(比如: myFunction = Function(myFunction.toString().replace(/Hello/, 'Hola'))),如果 myFunction 是一个闭包,那么修改不会生效(当然,尽管你可能都没这么想过)
  • 你可以的函数的函数声明中得到函数声明,同时闭包也如此。
  • 我认为通常闭包既是函数又是捕获的变量的术语。 请注意,我不在本文中使用该定义!
  • 我怀疑 javaScript 中的闭包与函数式语言中的闭包不同。

pic