来源 | http://www.codeceo.com/article/25-essential-Javascript-interview-questions.html
typeof bar === "object"
来确定 bar
是否是对象的潜在陷阱是什么?如何避免这个陷阱?尽管 typeof bar === "object"
是检查 bar
是否对象的可靠方法,令人惊讶的是在Javascript中 null
也被认为是对象!
因此,令大多数开发人员惊讶的是,下面的代码将输出 true
(而不是false
) 到控制台:
var bar = null;
console.log(typeof bar === "object"); // logs true!
只要清楚这一点,同时检查 bar
是否为 null
,就可以很容易地避免问题:
console.log((bar !== null) && (typeof bar === "object")); // logs false
要答全问题,还有其他两件事情值得注意:
首先,上述解决方案将返回 false
,当 bar
是一个函数的时候。在大多数情况下,这是期望行为,但当你也想对函数返回 true
的话,你可以修改上面的解决方案为:
console.log((bar !== null) && ((typeof bar === "object") || (typeof bar === "function")));
第二,上述解决方案将返回 true
,当 bar
是一个数组(例如,当 var bar = [];
)的时候。在大多数情况下,这是期望行为,因为数组是真正的对象,但当你也想对数组返回 false
时,你可以修改上面的解决方案为:
console.log((bar !== null) && (typeof bar === "object") && (toString.call(bar) !== "[object Array]"));
或者,如果你使用jQuery的话:
console.log((bar !== null) && (typeof bar === "object") && (! $.isArray(bar)));
(function(){
var a = b = 3;
})();
console.log("a defined? " + (typeof a !== 'undefined'));
console.log("b defined? " + (typeof b !== 'undefined'));
由于 a
和 b
都定义在函数的封闭范围内,并且都始于 var
关键字,大多数Javascript开发人员期望 typeof a
和 typeof b
在上面的例子中都是undefined。
然而,事实并非如此。这里的问题是,大多数开发人员将语句 var a = b = 3;
错误地理解为是以下声明的简写:
var b = 3;
var a = b;
但事实上,var a = b = 3;
实际是以下声明的简写:
b = 3;
var a = b;
因此(如果你不使用严格模式的话),该代码段的输出是:
a defined? false
b defined? true
但是, b
如何才能被定义在封闭函数的范围之外呢?是的,既然语句 var a = b = 3;
是语句 b = 3;
和 var a = b;
的简写, b
最终成为了一个全局变量(因为它没有前缀 var
关键字),因此仍然在范围内甚至封闭函数之外。
需要注意的是,在严格模式下(即使用 use strict
),语句var a = b = 3;
将生成ReferenceError: b is not defined
的运行时错误,从而避免任何否则可能会导致的headfakes /bug。 (还是你为什么应该理所当然地在代码中使用 use strict
的最好例子!)
var myObject = {
foo: "bar",
func: function() {
var self = this;
console.log("outer func: this.foo = " + this.foo);
console.log("outer func: self.foo = " + self.foo);
(function() {
console.log("inner func: this.foo = " + this.foo);
console.log("inner func: self.foo = " + self.foo);
}());
}
};
myObject.func();
上面的代码将输出以下内容到控制台:
outer func: this.foo = bar
outer func: self.foo = bar
inner func: this.foo = undefined
inner func: self.foo = bar
在外部函数中, this
和self
两者都指向了 myObject
,因此两者都可以正确地引用和访问 foo
。
在内部函数中, this
不再指向 myObject
。其结果是,this.foo
没有在内部函数中被定义,相反,指向到本地的变量self
保持在范围内,并且可以访问。 (在ECMA 5之前,在内部函数中的this
将指向全局的 window
对象;反之,因为作为ECMA 5,内部函数中的功能this
是未定义的。)
这是一个越来越普遍的做法,被许多流行的Javascript库(jQuery,Node.js等)采用。这种技术创建了一个围绕文件全部内容的闭包,也许是最重要的是,创建了一个私有的命名空间,从而有助于避免不同Javascript模块和库之间潜在的名称冲突。
这种技术的另一个特点是,允许一个易于引用的(假设更短的)别名用于全局变量。这通常用于,例如,jQuery插件中。jQuery允许你使用jQuery.noConflict()
,来禁用 $
引用到jQuery命名空间。在完成这项工作之后,你的代码仍然可以使用$
利用这种闭包技术,如下所示:
(function($) { /* jQuery plugin code referencing $ */ } )(jQuery);
use strict
有什么意义和好处?对于这个问题,既简要又最重要的答案是,use strict
是一种在Javascript代码运行时自动实行更严格解析和错误处理的方法。那些被忽略或默默失败了的代码错误,会产生错误或抛出异常。通常而言,这是一个很好的做法。
严格模式的一些主要优点包括:
使调试更加容易。那些被忽略或默默失败了的代码错误,会产生错误或抛出异常,因此尽早提醒你代码中的问题,你才能更快地指引到它们的源代码。
防止意外的全局变量。如果没有严格模式,将值分配给一个未声明的变量会自动创建该名称的全局变量。这是Javascript中最常见的错误之一。在严格模式下,这样做的话会抛出错误。
消除 this
强制。如果没有严格模式,引用null或未定义的值到 this
值会自动强制到全局变量。这可能会导致许多令人头痛的问题和让人恨不得拔自己头发的bug。在严格模式下,引用 null或未定义的 this
值会抛出错误。
不允许重复的属性名称或参数值。当检测到对象(例如,var object = {foo: "bar", foo: "baz"};
)中重复命名的属性,或检测到函数中(例如,function foo(val1, val2, val1){}
)重复命名的参数时,严格模式会抛出错误,因此捕捉几乎可以肯定是代码中的bug可以避免浪费大量的跟踪时间。
使eval()
更安全。在严格模式和非严格模式下,eval()
的行为方式有所不同。最显而易见的是,在严格模式下,变量和声明在 eval()
语句内部的函数不会在包含范围内创建(它们会在非严格模式下的包含范围中被创建,这也是一个常见的问题源)。
在 delete
使用无效时抛出错误。delete
操作符(用于从对象中删除属性)不能用在对象不可配置的属性上。当试图删除一个不可配置的属性时,非严格代码将默默地失败,而严格模式将在这样的情况下抛出异常。
function foo1()
{
return {
bar: "hello"
};
}
function foo2()
{
return
{
bar: "hello"
};
}
出人意料的是,这两个函数返回的内容并不相同。更确切地说是:
console.log("foo1 returns:");
console.log(foo1());
console.log("foo2 returns:");
console.log(foo2());
将产生:
foo1 returns:
Object {bar: "hello"}
foo2 returns:
undefined
这不仅是令人惊讶,而且特别让人困惑的是, foo2()
返回undefined却没有任何错误抛出。
原因与这样一个事实有关,即分号在Javascript中是一个可选项(尽管省略它们通常是非常糟糕的形式)。其结果就是,当碰到 foo2()
中包含 return
语句的代码行(代码行上没有其他任何代码),分号会立即自动插入到返回语句之后。
也不会抛出错误,因为代码的其余部分是完全有效的,即使它没有得到调用或做任何事情(相当于它就是是一个未使用的代码块,定义了等同于字符串 "hello"
的属性 bar
)。
这种行为也支持放置左括号于Javascript代码行的末尾,而不是新代码行开头的约定。正如这里所示,这不仅仅只是Javascript中的一个风格偏好。
NaN
是什么?它的类型是什么?你如何可靠地测试一个值是否等于 NaN
?NaN
属性代表一个“不是数字”的值。这个特殊的值是因为运算不能执行而导致的,不能执行的原因要么是因为其中的运算对象之一非数字(例如, "abc" / 4
),要么是因为运算的结果非数字(例如,除数为零)。
虽然这看上去很简单,但 NaN
有一些令人惊讶的特点,如果你不知道它们的话,可能会导致令人头痛的bug。
首先,虽然 NaN
意味着“不是数字”,但是它的类型,不管你信不信,是 Number
:
console.log(typeof NaN === "number"); // logs "true"
此外, NaN
和任何东西比较——甚至是它自己本身!——结果是false:
console.log(NaN === NaN); // logs "false"
一种半可靠的方法来测试一个数字是否等于 NaN,是使用内置函数 isNaN()
,但即使使用 isNaN()
依然并非是一个完美的解决方案。
一个更好的解决办法是使用 value !== value
,如果值等于NaN,只会产生true。另外,ES6提供了一个新的 Number.isNaN()
函数,这是一个不同的函数,并且比老的全局 isNaN()
函数更可靠。
console.log(0.1 + 0.2);
console.log(0.1 + 0.2 == 0.3);
一个稍微有点编程基础的回答是:“你不能确定。可能会输出“0.3”和“true”,也可能不会。Javascript中的数字和浮点精度的处理相同,因此,可能不会总是产生预期的结果。“
以上所提供的例子就是一个演示了这个问题的典型例子。但出人意料的是,它会输出:
0.30000000000000004
false
isInteger(x)
的可能方法,用于确定x是否是整数。这可能听起来是小菜一碟,但事实上,这很琐碎,因为ECMAScript 6引入了一个新的正以此为目的 Number.isInteger()
函数。然而,之前的ECMAScript 6,会更复杂一点,因为没有提供类似的 Number.isInteger()
方法。
问题是,在ECMAScript规格说明中,整数只概念上存在:即,数字值总是存储为浮点值。
考虑到这一点,最简单又最干净的ECMAScript6之前的解决方法(同时也非常稳健地返回 false
,即使一个非数字的值,如字符串或 null
,被传递给函数)如下:
function isInteger(x) { return (x^0) === x; }
下面的解决方法也是可行的,虽然不如上面那个方法优雅:
function isInteger(x) { return Math.round(x) === x; }
请注意 Math.ceil()
和 Math.floor()
在上面的实现中等同于 Math.round()
。
或:
function isInteger(x) { return (typeof x === 'number') && (x % 1 === 0);
相当普遍的一个不正确的解决方案是:
function isInteger(x) { return parseInt(x, 10) === x; }
虽然这个以 parseInt
函数为基础的方法在 x
取许多值时都能工作良好,但一旦 x
取值相当大的时候,就会无法正常工作。问题在于 parseInt()
在解析数字之前强制其第一个参数到字符串。因此,一旦数目变得足够大,它的字符串就会表达为指数形式(例如, 1e+21
)。因此,parseInt()
函数就会去解析 1e+21
,但当到达 e
字符串的时候,就会停止解析,因此只会返回值 1
。注意:
> String(1000000000000000000000)
'1e+21'
> parseInt(1000000000000000000000, 10)
1
> parseInt(1000000000000000000000, 10) === 1000000000000000000000
false
(function() {
console.log(1);
setTimeout(function(){console.log(2)}, 1000);
setTimeout(function(){console.log(3)}, 0);
console.log(4);
})();
序号如下:
1
4
3
2
让我们先来解释比较明显而易见的那部分:
1
和 4
之所以放在前面,是因为它们是通过简单调用 console.log()
而没有任何延迟输出的
2
之所以放在 3
的后面,是因为 2
是延迟了1000毫秒(即,1秒)之后输出的,而 3
是延迟了0毫秒之后输出的。
好的。但是,既然 3
是0毫秒延迟之后输出的,那么是否意味着它是立即输出的呢?如果是的话,那么它是不是应该在 4
之前输出,既然 4
是在第二行输出的?
要回答这个问题,你需要正确理解Javascript的事件和时间设置。
浏览器有一个事件循环,会检查事件队列和处理未完成的事件。例如,如果时间发生在后台(例如,脚本的 onload
事件)时,浏览器正忙(例如,处理一个 onclick
),那么事件会添加到队列中。当onclick处理程序完成后,检查队列,然后处理该事件(例如,执行 onload
脚本)。
同样的, setTimeout()
也会把其引用的函数的执行放到事件队列中,如果浏览器正忙的话。
当setTimeout()
的第二个参数为0的时候,它的意思是“尽快”执行指定的函数。具体而言,函数的执行会放置在事件队列的下一个计时器开始。但是请注意,这不是立即执行:函数不会被执行除非下一个计时器开始。这就是为什么在上述的例子中,调用 console.log(4)
发生在调用 console.log(3)
之前(因为调用 console.log(3)
是通过setTimeout被调用的,因此会稍微延迟)。
下面这个函数在 str
是回文结构的时候返回true,否则,返回false。
function isPalindrome(str) {
str = str.replace(/\W/g, '').toLowerCase();
return (str == str.split('').reverse().join(''));
}
例如:
console.log(isPalindrome("level")); // logs 'true'
console.log(isPalindrome("levels")); // logs 'false'
console.log(isPalindrome("A car, a man, a maraca")); // logs 'true'
12、写一个 sum
方法,在使用下面任一语法调用时,都可以正常工作。
console.log(sum(2,3)); // Outputs 5
console.log(sum(2)(3)); // Outputs 5
(至少)有两种方法可以做到:
方法1
function sum(x) {
if (arguments.length == 2) {
return arguments[0] + arguments[1];
} else {
return function(y) { return x + y; };
}
}
在Javascript中,函数可以提供到 arguments
对象的访问,arguments
对象提供传递到函数的实际参数的访问。这使我们能够使用 length
属性来确定在运行时传递给函数的参数数量。
如果传递两个参数,那么只需加在一起,并返回。
否则,我们假设它被以 sum(2)(3)
这样的形式调用,所以我们返回一个匿名函数,这个匿名函数合并了传递到 sum()
的参数和传递给匿名函数的参数。
方法2
function sum(x, y) {
if (y !== undefined) {
return x + y;
} else {
return function(y) { return x + y; };
}
}
当调用一个函数的时候,Javascript不要求参数的数目匹配函数定义中的参数数量。如果传递的参数数量大于函数定义中参数数量,那么多余参数将简单地被忽略。另一方面,如果传递的参数数量小于函数定义中的参数数量,那么缺少的参数在函数中被引用时将会给一个 undefined
值。所以,在上面的例子中,简单地检查第2个参数是否未定义,就可以相应地确定函数被调用以及进行的方式。
for (var i &#61; 0; i <5; i&#43;&#43;) {
var btn &#61; document.createElement(&#39;button&#39;);
btn.appendChild(document.createTextNode(&#39;Button &#39; &#43; i));
btn.addEventListener(&#39;click&#39;, function(){ console.log(i); });
document.body.appendChild(btn);
}
&#xff08;a&#xff09;当用户点击“Button 4”的时候会输出什么到控制台&#xff0c;为什么&#xff1f;&#xff08;b&#xff09;提供一个或多个备用的可按预期工作的实现方案。
&#xff08;a&#xff09;无论用户点击什么按钮&#xff0c;数字5将总会输出到控制台。这是因为&#xff0c;当 onclick
方法被调用&#xff08;对于任何按钮&#xff09;的时候&#xff0c; for
循环已经结束&#xff0c;变量 i
已经获得了5的值。&#xff08;面试者如果能够谈一谈有关如何执行上下文&#xff0c;可变对象&#xff0c;激活对象和内部“范围”属性贡有助于闭包行为&#xff0c;则可以加分&#xff09;。
&#xff08;b&#xff09;要让代码工作的关键是&#xff0c;通过传递到一个新创建的函数对象&#xff0c;在每次传递通过 for
循环时&#xff0c;捕捉到 i
值。下面是三种可能实现的方法&#xff1a;
for (var i &#61; 0; i <5; i&#43;&#43;) {
var btn &#61; document.createElement(&#39;button&#39;);
btn.appendChild(document.createTextNode(&#39;Button &#39; &#43; i));
btn.addEventListener(&#39;click&#39;, (function(i) {
return function() { console.log(i); };
})(i));
document.body.appendChild(btn);
}
或者&#xff0c;你可以封装全部调用到在新匿名函数中的 btn.addEventListener
&#xff1a;
for (var i &#61; 0; i <5; i&#43;&#43;) {
var btn &#61; document.createElement(&#39;button&#39;);
btn.appendChild(document.createTextNode(&#39;Button &#39; &#43; i));
(function (i) {
btn.addEventListener(&#39;click&#39;, function() { console.log(i); });
})(i);
document.body.appendChild(btn);
}
也可以调用数组对象的本地 forEach
方法来替代 for
循环&#xff1a;
[&#39;a&#39;, &#39;b&#39;, &#39;c&#39;, &#39;d&#39;, &#39;e&#39;].forEach(function (value, i) {
var btn &#61; document.createElement(&#39;button&#39;);
btn.appendChild(document.createTextNode(&#39;Button &#39; &#43; i));
btn.addEventListener(&#39;click&#39;, function() { console.log(i); });
document.body.appendChild(btn);
});
var arr1 &#61; "john".split(&#39;&#39;);
var arr2 &#61; arr1.reverse();
var arr3 &#61; "jones".split(&#39;&#39;);
arr2.push(arr3);
console.log("array 1: length&#61;" &#43; arr1.length &#43; " last&#61;" &#43; arr1.slice(-1));
console.log("array 2: length&#61;" &#43; arr2.length &#43; " last&#61;" &#43; arr2.slice(-1));
输出结果是&#xff1a;
"array 1: length&#61;5 last&#61;j,o,n,e,s"
"array 2: length&#61;5 last&#61;j,o,n,e,s"
arr1
和 arr2
在上述代码执行之后&#xff0c;两者相同了&#xff0c;原因是&#xff1a;
调用数组对象的 reverse()
方法并不只返回反顺序的阵列&#xff0c;它也反转了数组本身的顺序&#xff08;即&#xff0c;在这种情况下&#xff0c;指的是 arr1
&#xff09;。
reverse()
方法返回一个到数组本身的引用&#xff08;在这种情况下即&#xff0c;arr1
&#xff09;。其结果为&#xff0c;arr2
仅仅是一个到 arr1
的引用&#xff08;而不是副本&#xff09;。因此&#xff0c;当对 arr2
做了任何事情&#xff08;即当我们调用 arr2.push(arr3);
&#xff09;时&#xff0c;arr1
也会受到影响&#xff0c;因为 arr1
和 arr2
引用的是同一个对象。
这里有几个侧面点有时候会让你在回答这个问题时&#xff0c;阴沟里翻船&#xff1a;
传递数组到另一个数组的 push()
方法会让整个数组作为单个元素映射到数组的末端。其结果是&#xff0c;语句 arr2.push(arr3);
在其整体中添加 arr3
作为一个单一的元素到 arr2
的末端&#xff08;也就是说&#xff0c;它并没有连接两个数组&#xff0c;连接数组是 concat()
方法的目的&#xff09;。
和Python一样&#xff0c;Javascript标榜数组方法调用中的负数下标&#xff0c;例如 slice()
可作为引用数组末尾元素的方法&#xff1a;例如&#xff0c;-1下标表示数组中的最后一个元素&#xff0c;等等。
console.log(1 &#43; "2" &#43; "2");
console.log(1 &#43; &#43;"2" &#43; "2");
console.log(1 &#43; -"1" &#43; "2");
console.log(&#43;"1" &#43; "1" &#43; "2");
console.log( "A" - "B" &#43; "2");
console.log( "A" - "B" &#43; 2);
上面的代码将输出以下内容到控制台&#xff1a;
"122"
"32"
"02"
"112"
"NaN2"
NaN
原因是…
这里的根本问题是&#xff0c;Javascript&#xff08;ECMAScript&#xff09;是一种弱类型语言&#xff0c;它可对值进行自动类型转换&#xff0c;以适应正在执行的操作。让我们通过上面的例子来说明这是如何做到的。
例1&#xff1a;1 &#43; "2" &#43; "2"
输出&#xff1a;"122"
说明&#xff1a; 1 &#43; "2"
是执行的第一个操作。由于其中一个运算对象&#xff08;"2"
&#xff09;是字符串&#xff0c;Javascript会假设它需要执行字符串连接&#xff0c;因此&#xff0c;会将 1
的类型转换为 "1"
&#xff0c; 1 &#43; "2"
结果就是 "12"
。然后&#xff0c; "12" &#43; "2"
就是 "122"
。
例2&#xff1a; 1 &#43; &#43;"2" &#43; "2"
输出&#xff1a; "32"
说明&#xff1a;根据运算的顺序&#xff0c;要执行的第一个运算是 &#43;"2"
&#xff08;第一个 "2"
前面的额外 &#43;
被视为一元运算符&#xff09;。因此&#xff0c;Javascript将 "2"
的类型转换为数字&#xff0c;然后应用一元 &#43;
号&#xff08;即&#xff0c;将其视为一个正数&#xff09;。其结果是&#xff0c;接下来的运算就是 1 &#43; 2
&#xff0c;这当然是 3
。然后我们需要在一个数字和一个字符串之间进行运算&#xff08;即&#xff0c; 3
和 "2"
&#xff09;&#xff0c;同样的&#xff0c;Javascript会将数值类型转换为字符串&#xff0c;并执行字符串的连接&#xff0c;产生 "32"
。
例3&#xff1a; 1 &#43; -"1" &#43; "2"
输出&#xff1a; "02"
说明&#xff1a;这里的解释和前一个例子相同&#xff0c;除了此处的一元运算符是 -
而不是 &#43;
。先是 "1"
变为 1
&#xff0c;然后当应用 -
时又变为了 -1
&#xff0c;然后将其与 1
相加&#xff0c;结果为 0
&#xff0c;再将其转换为字符串&#xff0c;连接最后的 "2"
运算对象&#xff0c;得到 "02"
。
例4&#xff1a; &#43;"1" &#43; "1" &#43; "2"
输出&#xff1a; "112"
说明&#xff1a;虽然第一个运算对象 "1"
因为前缀的一元 &#43;
运算符类型转换为数值&#xff0c;但又立即转换回字符串&#xff0c;当连接到第二个运算对象 "1"
的时候&#xff0c;然后又和最后的运算对象"2"
连接&#xff0c;产生了字符串 "112"
。
例5&#xff1a; "A" - "B" &#43; "2"
输出&#xff1a; "NaN2"
说明&#xff1a;由于运算符 -
不能被应用于字符串&#xff0c;并且 "A"
和 "B"
都不能转换成数值&#xff0c;因此&#xff0c;"A" - "B"
的结果是 NaN
&#xff0c;然后再和字符串 "2"
连接&#xff0c;得到 "NaN2"
。
例6&#xff1a; "A" - "B" &#43; 2
输出&#xff1a; NaN
说明&#xff1a;参见前一个例子&#xff0c; "A" - "B"
结果为 NaN
。但是&#xff0c;应用任何运算符到NaN与其他任何的数字运算对象&#xff0c;结果仍然是 NaN
。
var list &#61; readHugeList();
var nextListItem &#61; function() {
var item &#61; list.pop();
if (item) {
// process the list item...
nextListItem();
}
};
潜在的堆栈溢出可以通过修改nextListItem
函数避免&#xff1a;
var list &#61; readHugeList();
var nextListItem &#61; function() {
var item &#61; list.pop();
if (item) {
// process the list item...
setTimeout( nextListItem, 0);
}
};
堆栈溢出之所以会被消除&#xff0c;是因为事件循环操纵了递归&#xff0c;而不是调用堆栈。当 nextListItem
运行时&#xff0c;如果 item
不为空&#xff0c;timeout函数&#xff08;nextListItem
&#xff09;就会被推到事件队列&#xff0c;该函数退出&#xff0c;因此就清空调用堆栈。当事件队列运行其timeout事件&#xff0c;且进行到下一个 item
时&#xff0c;定时器被设置为再次调用 nextListItem
。因此&#xff0c;该方法从头到尾都没有直接的递归调用&#xff0c;所以无论迭代次数的多少&#xff0c;调用堆栈保持清空的状态。
闭包是一个可以访问外部&#xff08;封闭&#xff09;函数作用域链中的变量的内部函数。闭包可以访问三种范围中的变量&#xff1a;这三个范围具体为&#xff1a;&#xff08;1&#xff09;自己范围内的变量&#xff0c;&#xff08;2&#xff09;封闭函数范围内的变量&#xff0c;以及&#xff08;3&#xff09;全局变量。
下面是一个简单的例子&#xff1a;
var globalVar &#61; "xyz";
(function outerFunc(outerArg) {
var outerVar &#61; &#39;a&#39;;
(function innerFunc(innerArg) {
var innerVar &#61; &#39;b&#39;;
console.log(
"outerArg &#61; " &#43; outerArg &#43; "\n" &#43;
"innerArg &#61; " &#43; innerArg &#43; "\n" &#43;
"outerVar &#61; " &#43; outerVar &#43; "\n" &#43;
"innerVar &#61; " &#43; innerVar &#43; "\n" &#43;
"globalVar &#61; " &#43; globalVar);
})(456);
})(123);
在上面的例子中&#xff0c;来自于 innerFunc
&#xff0c; outerFunc
和全局命名空间的变量都在 innerFunc
的范围内。因此&#xff0c;上面的代码将输出如下&#xff1a;
outerArg &#61; 123
innerArg &#61; 456
outerVar &#61; a
innerVar &#61; b
globalVar &#61; xyz
for (var i &#61; 0; i <5; i&#43;&#43;) {
setTimeout(function() { console.log(i); }, i * 1000 );
}
解释你的答案。闭包在这里能起什么作用&#xff1f;
上面的代码不会按预期显示值0&#xff0c;1&#xff0c;2&#xff0c;3&#xff0c;和4&#xff0c;而是会显示5&#xff0c;5&#xff0c;5&#xff0c;5&#xff0c;和5。
原因是&#xff0c;在循环中执行的每个函数将整个循环完成之后被执行&#xff0c;因此&#xff0c;将会引用存储在 i
中的最后一个值&#xff0c;那就是5。
闭包可以通过为每次迭代创建一个唯一的范围&#xff0c;存储范围内变量的每个唯一的值&#xff0c;来防止这个问题&#xff0c;如下&#xff1a;
for (var i &#61; 0; i <5; i&#43;&#43;) {
(function(x) {
setTimeout(function() { console.log(x); }, x * 1000 );
})(i);
}
这就会按预期输出0&#xff0c;1&#xff0c;2&#xff0c;3&#xff0c;和4到控制台。
console.log("0 || 1 &#61; "&#43;(0 || 1));
console.log("1 || 2 &#61; "&#43;(1 || 2));
console.log("0 && 1 &#61; "&#43;(0 && 1));
console.log("1 && 2 &#61; "&#43;(1 && 2));
并解释。
该代码将输出&#xff1a;
0 || 1 &#61; 1
1 || 2 &#61; 1
0 && 1 &#61; 0
1 && 2 &#61; 2
在Javascript中&#xff0c; ||
和 &&
都是逻辑运算符&#xff0c;用于在从左至右计算时&#xff0c;返回第一个可完全确定的“逻辑值”。
或&#xff08; ||
&#xff09;运算符。在形如 X||Y
的表达式中&#xff0c;首先计算X
并将其解释执行为一个布尔值。如果这个布尔值true
&#xff0c;那么返回true
&#xff08;1&#xff09;&#xff0c;不再计算 Y
&#xff0c;因为“或”的条件已经满足。如果这个布尔值为false
&#xff0c;那么我们仍然不能知道 X||Y
是真是假&#xff0c;直到我们计算 Y
&#xff0c;并且也把它解释执行为一个布尔值。
因此&#xff0c; 0 || 1
的计算结果为true&#xff08;1&#xff09;&#xff0c;同理计算1 || 2
。
与&#xff08; &&
&#xff09;运算符。在形如 X&&Y
的表达式中&#xff0c;首先计算 X
并将其解释执行为一个布尔值。如果这个布尔值为 false
&#xff0c;那么返回 false
&#xff08;0&#xff09;&#xff0c;不再计算 Y
&#xff0c;因为“与”的条件已经失败。如果这个布尔值为true
&#xff0c;但是&#xff0c;我们仍然不知道 X&&Y
是真是假&#xff0c;直到我们去计算 Y
&#xff0c;并且也把它解释执行为一个布尔值。
不过&#xff0c;关于 &&
运算符有趣的地方在于&#xff0c;当一个表达式计算为“true”的时候&#xff0c;那么就返回表达式本身。这很好&#xff0c;虽然它在逻辑表达式方面计算为“真”&#xff0c;但如果你希望的话也可用于返回该值。这就解释了为什么&#xff0c;有些令人奇怪的是&#xff0c; 1 && 2
返回 2
&#xff08;而不是你以为的可能返回 true
或 1
&#xff09;。
console.log(false &#61;&#61; &#39;0&#39;)
console.log(false &#61;&#61;&#61; &#39;0&#39;)
代码将输出&#xff1a;
true
false
在Javascript中&#xff0c;有两种等式运算符。三个等于运算符 &#61;&#61;&#61;
的作用类似传统的等于运算符&#xff1a;如果两侧的表达式有着相同的类型和相同的值&#xff0c;那么计算结果为true。而双等于运算符&#xff0c;会只强制比较它们的值。因此&#xff0c;总体上而言&#xff0c;使用 &#61;&#61;&#61;
而不是 &#61;&#61;
的做法更好。 !&#61;&#61;
vs !&#61;
亦是同理。
var a&#61;{},
b&#61;{key:&#39;b&#39;},
c&#61;{key:&#39;c&#39;};
a[b]&#61;123;
a[c]&#61;456;
console.log(a[b]);
这段代码将输出 456
&#xff08;而不是 123
&#xff09;。
原因为&#xff1a;当设置对象属性时&#xff0c;Javascript会暗中字符串化参数值。在这种情况下&#xff0c;由于 b
和 c
都是对象&#xff0c;因此它们都将被转换为"[object Object]"
。结果就是&#xff0c; a[b]
和a[c]
均相当于a["[object Object]"]
&#xff0c;并可以互换使用。因此&#xff0c;设置或引用 a[c]
和设置或引用 a[b]
完全相同。
console.log((function f(n){return ((n > 1) ? n * f(n-1) : n)})(10));
并解释你的答案。
代码将输出10&#xff01;的值&#xff08;即10&#xff01;或3628800&#xff09;。
原因是&#xff1a;
命名函数 f()
递归地调用本身&#xff0c;当调用 f(1)
的时候&#xff0c;只简单地返回1
。下面就是它的调用过程&#xff1a;
f(1): returns n, which is 1
f(2): returns 2 * f(1), which is 2
f(3): returns 3 * f(2), which is 6
f(4): returns 4 * f(3), which is 24
f(5): returns 5 * f(4), which is 120
f(6): returns 6 * f(5), which is 720
f(7): returns 7 * f(6), which is 5040
f(8): returns 8 * f(7), which is 40320
f(9): returns 9 * f(8), which is 362880
f(10): returns 10 * f(9), which is 3628800
23、请看下面的代码段。控制台将输出什么&#xff0c;为什么&#xff1f;
(function(x) {
return (function(y) {
console.log(x);
})(2)
})(1);
控制台将输出 1
&#xff0c;即使从来没有在函数内部设置过x
的值。原因是&#xff1a;
正如我们在Javascript招聘指南中解释过的那样&#xff0c;闭包是一个函数&#xff0c;连同在闭包创建的时候&#xff0c;其范围内的所有变量或函数一起。在Javascript中&#xff0c;闭包是作为一个“内部函数”实施的&#xff1a;即&#xff0c;另一个函数主体内定义的函数。闭包的一个重要特征是&#xff0c;内部函数仍然有权访问外部函数的变量。
因此&#xff0c;在本例中&#xff0c;由于 x
未在函数内部中定义&#xff0c;因此在外部函数范围中搜索定义的变量 x
&#xff0c;且被发现具有1
的值。
var hero &#61; {
_name: &#39;John Doe&#39;,
getSecretIdentity: function (){
return this._name;
}
};
var stoleSecretIdentity &#61; hero.getSecretIdentity;
console.log(stoleSecretIdentity());
console.log(hero.getSecretIdentity());
代码有什么问题&#xff0c;以及应该如何修复。
代码将输出&#xff1a;
undefined
John Doe
第一个 console.log
之所以输出 undefined
&#xff0c;是因为我们正在从 hero
对象提取方法&#xff0c;所以调用了全局上下文中&#xff08;即窗口对象&#xff09;的 stoleSecretIdentity()
&#xff0c;而在此全局上下文中&#xff0c; _name
属性不存在。
其中一种修复stoleSecretIdentity()
函数的方法如下&#xff1a;
var stoleSecretIdentity &#61; hero.getSecretIdentity.bind(hero);
此函数的参数为&#xff1a;
DOM元素
回调函数&#xff08;将DOM元素作为其参数&#xff09;
访问树&#xff08;DOM&#xff09;的所有元素是经典的深度优先搜索算法应用。下面是一个示范的解决方案&#xff1a;
function Traverse(p_element,p_callback) {
p_callback(p_element);
var list &#61; p_element.children;
for (var i &#61; 0; i <list.length; i&#43;&#43;) {
Traverse(list[i],p_callback); // recursive call
}
}