1.使用 typeof bar === “object” 来确定 bar 是否是一个对象时有什么潜在的缺陷?这个缺陷如何避免?
尽管 typeof bar ===“object” 是检测 bar 是否是对象的可靠的方法,但是在 JavaScript 中令人惊讶的问题是 null 也被认为是一个对象
因此,对于大部分开发人员来说,下面的代码将会是 true (而不是 false)
var bar = null ;
console.log(typeof bar === "object"); // logs true
只要知道这一点就可以通过检测 bar 是否为空来轻松避免该问题:
console.log((bar != null) && (typeof bar === "object")); // logs false
为了让我们的答案更加的完整,还有两件事值得注意:
首先,如果 bar 是一个函数,上面的解决方案将会返回 false ,在大多数情况下是所期望的,但是在您希望函数返回 true 的情况下,您可以将上述解决方案修改为:
console.log((bar != null) && ((typeof bar === "object") || (typeof bar === "function"))); // logs true
但是还有一个替代方法对空值,数组和函数返回 false,但对于对象则为 true:
console.log((bar != null) && (bar.constructor === Object)); // logs true
constructor 返回创建实例对象的 Object 构造函数的引用
var o = {};
o.constructor === Object; // true
var o = new Object;
o.constructor === Object; // true
var a = [];
a.constructor === Array; // true
var a = new Array;
a.constructor === Array; // true
var n = new Number(3);
n.constructor === Number; // true
或者,使用 jQuery:
console.log((bar != null) && (bar.constructor === "object") && (! $.isArray(bar)); // logs true
ES5 使得数组的情况非常简单,包括它自己的空检查:
console.log(Array.isArray(bar)); // logs true
2、下面代码将在控制台输出什么?为什么?
(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 在上面的事例中都为定义。
但是,情况并不是这样。这里的问题是大多数开发人员错误的理解语句 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
究其原因,是因为 var a = b = 3,是语句 b = 3 的简写,并且还声明了 var a = b;因此 b 最终成为了一个全局变量(因为它不在 var 关键词后面),所以它仍然在作用域内。
注:在严格模式下(即,使用 strict),语句 var a = b = 3 会产生一个 ReferenceError 的运行时错误:b 没有定义,从而可以避免类似的错误(由此可见,在代码中使用 strict 还是很重要的)
3、下面的代码将在控制台输出什么?为什么?
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: this.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。
但是在内部函数中,这不在指向 myObject 。因此,this.foo 在内部函数中时未定义的,而对局部变量 self 的引用仍然在范围内并且指向 myObject。
4、在功能模块中将 JavaScript 源文件进行封装的重要性及原因?
这是一种日益普遍的做法,被许多流行的 JavaScript 库(jQuery、Node.js 等)所采用。这种技术的实现是在整个文件的周围创建一个闭包,这主要是为了创建一个私有的命名空间,从而有助于避免不同 JavaScript 模块和库之间的潜在名称冲突。
这种技术的另一个特点是为全局变量提供一个容易引用的别名,例如,通常使用的 jQuery 就是用了 ‘$’,当然我们也可以使用 jQuery.noConflict() 来禁用 jQuery 对 $ 的控制权,以使得我们的代码中仍然可以使用 $ 作为闭包的别名。
5、在 JavaScript 文件中使用 ‘use strict’ 的意义和好处?
“use strict” 顾名思义就是使用严格模式的意思,它是一种在运行时自动执行更严格的 JavaScript 代码解析和错误处理的方法,如果代码错误被忽略,将会在控制台抛出异常,总而言之这是非常好的做法。
严格模式的一些主要优点包括:
(1)使调试更加容易:不使用严格模式时一些代码错误会被忽略,那么现在将会产生错误或抛出异常,从而使我们能够更快的发现代码中的问题
(2)防止意外全局:如果没有严格模式,赋值给未声明的变量会自动创建一个具有该名称的全局变量,这是 JavaScript 中最常见的错误之一,在严格模式下,这样做会引发错误
(3)消除隐藏威胁:在没有严格模式的情况下,对 null 或 undefined 的引用会自动强制到全局,这可能会导致许多错误,在严格模式下,引用 null 或 undefined 的这个值会引发错误
(4)不允许重复的参数值:在严格模式下,重复命名参数会引发错误(例如,函数 foo(val1,val2,val1) {}),这样使我们可以轻易的找到存在的错误,不必浪费大量的时间去追踪问题
注:它曾经也会禁止重复的属性名称(例如,var object = {foo:‘bar’,foo:‘baz’}),但是从 ECMAScript 2015 开始,就不在有这种情况了
(5)使 eval() 更安全:在严格模式下,在 eval() 语句内部声明的变量和函数不会覆盖在外部声明的同名变量
(6)抛出使用错误的删除符:删除操作符(用于从对象中删除属性)不能用于删除对象中不可配置的属性,当试图删除一个不可配置的属性时,非严格模式代码将自动失败,而在严格模式下会引发错误
6、考虑下面两个函数,它们会返回同样的值吗?为什么?
function fool() {
return {
bar : "hello"
}
}
function fool2(){
return
{
bar: "hello"
}
}
我想大家咋看之下一定会认为输出的结果是一样,我一开始也是这么认为的。但是一看输出的结果简直让我惊讶
console.log("fool return:"); // fool return:
console.log(fool()); // Object {bar : "hello"}
console.log("fool2 return:"); // fool2 return:
console.log(fool2()); // undefined
事实上,这与 JavaScript 中分号是可选的有关(忽略它们通常是非常糟糕的形式,因此最好都写上)。在 fool2() 中遇到包含 return 的语句的行(没有其它内容)时,会在 return 语句后立即自动插入分号,因此 return 出来的就是一个 undefined。
由于代码的其余部分是完全有效的,即使它没有被调用或做任何事也不会报错。
在写大括号时,我们还应该遵循 JavaScript 中的另一个约定,即将一行开头的大括号放在行尾,而不是在新行的开头。
7、什么是 NaN?它的类型是什么?如何可靠的测试一个值是否等于 NaN?
NaN 属性表示“不是数字”的值,这个特殊值是由于进行运算时操作数是一个非数字的或者是因为操作不当而导致结果是非数字而无法继续执行
虽然这些看起来很简单,但 NaN 也有一些令人惊讶的特征,如果没有意识到这个就就有可能导致 bug
首先,NaN 的意思虽然是“不是数字”,但它的类型却是数字:
console.log(typeof NaN === "number"); // true
此外,NaN 相比其他任何事情甚至是它本身,结果都是 false
console.log( NaN === NaN); // false
我们可以通过 ES6 提供的新的 Number.isNaN() 函数去检测一个数字是不是 NaN
8、下面的代码输出什么?为什么?
console.log(0.1 + 0.2);
console.log(0.1 + 0.2 == 0.3);
对于这个问题一个有教养的回答是:“我不确定,它可能打印出 0.3 和 true,或者不是。JavaScript 中的数字全部使用浮点精度处理,因此可能不会总是产生预期的结果。”
上面的事例打印出来的结果有可能是这样(当然也可能不是):
0.30000000000000004
false
一个经典的解决方案是比较两个数字与特殊常数 Number.EPSILON 之间的绝对差值(如果返回 true 则可以说这两个数值是相等的):
function areTheNumbersAlmostEqual(num1 , num2){
return Math.abs (num1 - num2) < Number.EPSILON;
};
console.log(areTheNumbersAlmostEqual(0.1 + 0.2 , 0.3));
9、判断一个数是否为整数?
在 ES6 中拥有一个新的 Number.isInteger() 函数,它可以用来判断一个数值是否是一个整数。
但是在 ES6 之前,想要判断就有点复杂,因为在 ECMAScript 规范中,整数只是概念上的存在,即数值始终作为浮点值存储。
因此,最简单也是最清洁的 ES6 之前的解决方案(即使将非数字值(例如字符串或空值)传递给该函数,该方案也具有足够的可靠性返回 false)就是使用按位异或运算符:
function isInteger(x){
return (x ^ 0) === x ;
};
除此之外还可以用一下方法进行判断,尽管不如上面看着那么牛逼:
function isInteger(x){
return Math.round(x) === x ;
};
注:上面的实现中 Math.ceil() 或 Math.floor() 同样可以使用
最后还有一个方法也是可以判断的:
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
10、执行下面的代码时,将按什么顺序将数字 1-4 记录到控制台?为什么?
(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 而没有任何延迟记录的。
2 肯定是在 3 之后显示,因为 2 的记录是要在延迟 1000 毫秒后。
那么,问题来了,延迟 0 毫秒后记录 3,这是否意味着它正在被立即记录?如果是这样的话,不应该是在 4 之前记录它吗,因为 4 是由后面的代码行记录的?
答案与正确理解 JavaScript 事件和时间有关。
浏览器又一个时间循环,它检查时间队列并处理未解决的事件。例如,如果在浏览器繁忙时(例如:处理 onclick)在后台发生事件(例如脚本 onload 事件),则该事件会被附加到队列中。当 onclick 处理程序完成时,将检查队列并处理该事件(例如脚本 onload 事件)。
同样,如果浏览器繁忙,setTimeout() 也会将其引用函数的执行放入事件队列中,因此,setTimeout() 会在浏览器执行的事件执行完毕后在执行,所以,最后的结果就会如上图所示。
11、编写一个简单的函数(少于 160 个字符),返回一个布尔值,指示字符串是否是一个回文
如何检测回文,其实在我理解就是如果一个字符串翻转过来,能和原字符串完全相等,那么就可以称之为回文,因此可以使用如下方法来检查 str 是否是一个回文,是则返回 true,否则返回 false。
function isPalindrome(str){
str = str.replace(/\W/g,'').toLowerCase();
return (str == str.split('').reverse().join(''));
};
例如:
console.log(isPalindrome("level")); // true
console.log(isPalindrome("levels")); // false
console.log(isPalindrome("A car , a man , a maraca")); // true
12、写一个 sum 方法,当时用下面语法调用时它能正常工作
console.log(sum(2,3));
console.log(sum(2)(3));
有两种方法可以实现这个:
方法一:
function sum(x){
if (arguments.length == 2){
return arguments[0] + argumengts[1];
} else {
return function(y) { return x + y};
}
};
在 JavaScript 中,有一个 arguments 属性,它是用来显示函数调用时传递参数的,因此我们可使用它的 length 属性来确认传入的参数的数量,如果传递的是两个参数,我们只需将它们返回并相加;如果我们是以 sum(2)(3) 的方式被调用的,那么我们就返回一个匿名函数,它将传递给 sum() 的参数传递给匿名函数。
方法二:
function sum(x,y){
if (y ! = umdefined){
return x + y;
} else {
return function(y) { return x + y};
}
};
在函数被调用时,JavaScript 不需要参数的数量匹配函数定义中参数的数量。如果传递的参数数量超过了定义的参数的数量,那么超过的部分将被忽略;如果传递的参数数量小于定义的参数的数量,在函数内引用时,缺少的参数将是未定义。因此,也可以通过第二个参数是否定义来确定函数的调用方式。
12、考虑下面的代码的输出结果
for (var i = 0; i < 5; i++) {
var btn = document.createElement('button');
btn.appendChild(document.createTextNode(' Button ' + i));
btn.addEvenListener('click',function(){ console.log(i) });
document.body.appendChild(btn);
};
问:
1、当用户点击“按钮4”时,什么被记录到控制台?为什么?
2、提供一个或多个可预期的工作进行代替
答:
1、无论点击那个按钮,数字 5 将始终被记录在控制台。这是因为,在调用 onclick 方法时,for 循环已经完成,并且变量 i 已经是数字 5。
2、有以下 4 中方法可以进行代替
方法一:
for (var i = 0; i < 5; i++) {
var btn = document.createElement('button');
btn.appendChild(document.createTextNode(' Button ' + i));
btn.addEvenListener('click',(function (i) {
return function() { console.log(i); }
})(i));
document.body.appendChild(btn);
};
该方法通过将 i 传递给新创将的函数来捕获每次通过 for 循环的 i 的值。
方法二:
for (var i = 0; i < 5; i++) {
var btn = document.createElement('button');
btn.appendChild(document.createTextNode(' Button ' + i));
(function (i) {
btn.addEvenListener('click', function () { console.log(i) };)
})(i);
document.body.appendChild(btn);
};
该方法使用自调用函数,将整个的 btn.addEvenListener 包装在其中。
方法三:
['a', 'b', 'c', 'd', 'e'].forEach (function (value , i) {
var btn = document.createElement('button');
btn.appendChild(document.createTextNode(' Button ' + i));
btn.addEvenListener('click', function () { console.log(i) };)
document.body.appendChild(btn);
};
该方法通过调用数组对象的原生 forEach 方法来替换 for 循环。
方法四:
for (let i = 0; i < 5; i++) {
var btn = document.createElement('button');
btn.appendChild(document.createTextNode(' Button ' + i));
btn.addEvenListener('click',(function (i) {
return function() { console.log(i); }
})(i));
document.body.appendChild(btn);
};
最后,也是最简单的方法就是使用 let 代替 var (前提是在 ES6 / ES2015 的上下文中,如果不是需要使用 babel 来将 ES6 转换为 ES5 的语法)。
13、假设 d 是范围内的空对象
var d = {};
使用下面的代码完成了什么?
['zebra', 'horse'].forEach (function (k) {
d[k] = undefined;
};
输出的结果为:
var d = {
zebra: undefined;
horse: undefined;
};
上面的代码在对象 d 中设置了两个属性,这是为了确保对象具有一组给定的属性的有用策略,将该对象传递给 Object.keys 将返回一个包含这些设置键的数组(即使它们的值是未定义)
注:Object.keys 将返回一个数组,如果传入的是对象将返回属性名;如果传入的是字符串,则返回的是索引;如果是构造函数,将返回空数组或者属性名;如果传入的是数组,将返回索引。
14、下面的代码将在控制台输出什么?为什么?
var arr1 = "john".split('');
var arr2 = arr1.reverse();
var arr3 = "jones".split('');
arr2.push(arr3);
console.log("array 1:length=" + arr1.length + "last=" + arr1.slice(-1));
console.log("array 2:length=" + arr2.length + "last=" + arr2.slice(-1));
输出的结果是:
"array1: length=5 last=j,o,n,e,s";
"array2: length=5 last=j,o,n,e,s";
arr1 和 arr2 是相同的(即 [‘n’,‘h’,‘o’,‘j’,[‘j’,‘o’,‘n’,‘e’,‘s’]]),之所以这样原因是:
(1) 调用数组对象的 reverse() 方法不仅以相反的顺序返回数组,它还颠倒了数组本身的顺序(即 arr1 的顺序)
(2) reverse() 返回对数组本身的引用(即对 arr1 的引用)。因此,arr2 仅是对 arr1 的引用而不是副本。因此,当对 arr2 做任何事情时,arr1 也会受到影响,因为 arr1 和 arr2 只是对同一个对象的引用
还有几个需要注意的点:
(1) 将数组传递给另一个数组的 push() 方法会将整个数组作为一个单元推入数组的末端,结果就是,将 arr3 作为一个整体添加到 arr2 的末尾(它不是连接两个数组,这就是会有 concat() 方法的原因)
(2) 像 Python 一样, JavaScript 在调用像 slice() 这样的数组方法时,会承认负数下标,以此作为在数组末尾引用元素的方式,例如:下标为 -1 就表示数组的倒数第一个元素,以此类推
15、下面的代码将在控制台输出什么?为什么?
console.log(1 + "2" + "2");
console.log(1 + +"2" + "2");
console.log(1 + -"1" + "2");
console.log(+"1" + "1" + "2");
console.log("A" - "B" + "2");
console.log("A" - "B" + 2);
以上代码的输出结果是:
"122";
"32";
"02";
"112";
"NaN2";
NaN;
这里的基本问题是 JavaScript 是一种松散型的语言,它对值执行自动类型转换以适应正在执行的操作。
实例一:第一个操作其实是在 1 + “2” 中执行,由于其中一个操作数(“2”)是一个字符串,所以 JavaScript 假定需要执行字符串连接,因此将 1 的类型转换为 “1”,所以 1 + “2” 会转换为 “12”。然后,“12” + “2” 产生 “122”。
实例二:根据操作顺序,要执行的第一个操作是 “+2”(第一个 “2” 之前的 “+” 被视作一个一元运算符)。因此,JavaScript 将 “+2” 的类型转换为数字,即为 +2,下一步操作就是 1+2,这个就会产生 3,之后的操作是一个数字和一个字符串之间的操作(即 3 和 “2”),所以 JavaScript 再次转换数值变为一个字符串之后再与 “2” 执行字符串的连接,就会产生 “32”。
实例三:这一个与上一个实例是相同的,只不过将一元运算符换成了"-",因此,“1” 变成了 1,然后使用 “-” 将其变为 “-1”,之后执行加 1 结果就变成了 0,最后在于字符串 “2” 进行拼串,其结果就是 “02”。
实例四:尽管第一个 “1” 前面有一元操作符 “+”,但第二个 “1” 是一个字符串,其操作的结果就是进行拼串,输出 “12”,后面的 “2” 也是一个字符串,因此又会进行拼串操作,最终返回 “112”。
实例五:虽然 “-” 运算符能够将字符串转换为数字,但是也仅仅是将数字类的字符串进行转换,对于本实例中的 “A” “B” 是无法转换的,因此执行 “-” 时就会产生 NaN,而后面的 “2” 是一个字符串,且执行的是 “+”,所以 NaN 会与 “2” 进行拼串,最后输出的结果就是 “NaN2”。
实例六:和上一个实例一样,“A” - “B” 结果是 NaN,但其后面的 “2” 是一个数字,而运算符是 “+”,因为 NaN 与任何数字进行操作都会返回 NaN,所以本例的结果是 NaN。
16、如果数组列表太大,以下递归代码将导致堆栈溢出。你如何解决这个问题,仍然保留递归模式。
var list = readHugeList();
var nextListItem = function() {
var item = list.pop();
if(item) {
nextListItem();
}
};
注:数组的 pop() 方法,删除数组的最后一项,将数组的长度减 1,并且返回它删除的元素的值。如果数组已经为空,则 pop() 不改变数组,并且返回 undefined
我们可以通过下面的方法避免堆栈溢出,如下所示:
var list = readHugeList();
var nextListItem = function() {
var item = list.pop();
if(item) {
setTimeout(nextListItem , 0);
};
};
上述方法之所以可以消除堆栈溢出,是因为上面方法造成堆栈溢出的原因是由于函数不断循环,如果数组过大,将会导致函数长时间执行而不能结束,使用 setTimeout 方法,可以在代码执行到此处时,将其推送到事件队列,从而使得函数结束,在事件队列运行超时时再次处理函数,这就有效的避免了堆栈的溢出。
17、什么是 JavaScript 中的闭包?举一个例子。
闭包是一个内部函数,它可以访问外部(封闭)函数的作用域链中的变量。闭包可以访问三个范围内的变量,具体来说:
(1) 变量在其自己的范围内
(2) 封闭函数范围内的变量
(3) 全局变量
举个例子
var globalVar = "xyz";
(function outerFunc(outerArg) {
var outerVar = "a";
(function innerFunc(innerArg) {
var innerVar = "b";
console.log(
"outerArg = " + outerArg + "\n" +
"innerArg = " + innerArg + "\n" +
"outerVar = " + outerVar + "\n" +
"innerVar = " + innerVar + "\n" +
"globalVar = " + globalVar
);
})(456);
})(123);
上面的例子在控制台输出的结果是:
outerArg = 123;
innerArg = 456;
outerVar = a;
innerVar = b;
globalVar = xyz;
之所以会输出这样的结果,是因为函数 outerFunc() 和 innerFunc() 都在函数 innerFunc() 的范围内,其变量均可以访问到,而 globalVar 是全局变量,所以也可以访问到。
18、以下代码的输出是什么?
for (var i = 0 ; i < 5 ; i++){
setTimeout(function () { console.log(i) } , i * 1000);
}
这段代码输出的结果显然不会是 0,1,2,3,4,而是全部是 5,具体的原因之前已经说到过了,就是因为循环内执行的函数将在整个 for 循环执行完毕后执行,因此所有函数都会引用存储在 i 中的最后一个值,即 5 。
我们可以通过闭包的方式来解决这个问题,每次循环时都会创建一个唯一的作用域,并将该变量的每个唯一值存储在其作用域中,如下图所示:
for (var i = 0 ; i < 5 ; i++){
(function(x) {
setTimeout(function() {conosle.log(x)}, x * 1000);
})(i);
}
除此之外,我们还可以使用 ES2015 的 let 代替 var :
for (let i = 0 ; i < 5 ; i++){
setTimeout(function () { console.log(i) } , i * 1000);
}
19、以下代码输出的结果?
console.log(" 0 || 1 = " + (0 || 1));
console.log(" 1 || 2 = " + (1 || 2));
console.log(" 0 && 1 = " + (0 && 1));
console.log(" 1 && 2 = " + (1 && 2));
上面代码在控制输出的结果为:
0 || 1 = 1;
1 || 2 =1;
0 && 1 = 0;
1 && 2 = 2;
在 JavaScript 中,|| 和 && 都是逻辑运算符,当从左到右计算时返回第一个完全确定的“逻辑值”。
或(||)运算符,在形式为 X || Y 的表达式中,首先计算 X 并将其解释为布尔值,如果为真,则返回 X,并且不再计算 Y,但是,如果为假,则会继续去计算,如果 Y 为真则返回 Y,如果 Y 依旧为假,也会返回 Y 。总之或(||)运算符,会去找到为真的项之后返回该项,否则则会一直执行完,返回最后的项。
与(&&)运算符,在形式为 X && Y 的表达式中,首先计算 X 并将其解释为布尔值,如果为假,则停止计算并返回 X,否则则会一直执行直到找到假,如果没有假,则返回最后一项。
20、下面代码执行时会输出什么?为什么?
console.log(false == '0');
console.log(false === '0');
该代码将输出:
true;
false;
在 JavaScript 中,有两套相等运算符。三重相等运算符 ===
是对比等号两边的表达式的类型与值,只有都相等时其结果才为 true。而双等运算符 ==
则是现将等号两边的表达式转换为相同类型,之后比较它们的值是否相等,只要值相等就返回 true。
21、以下代码的输出是什么?为什么?
var a = {},
b = {key : 'b'},
c = {key : 'c'};
a[b] = 123;
a[c] = 456;
console.log(a[b]);
此代码输出的结果将是 456 而不是 123。
原因如下:设置对象属性时,JavaScript 会隐式的将参数值串联起来。在这种情况下,由于 b 和 c 都是对象,它们都将被转换为[object Object]
。因此,a[b] 和 a[c] 都等价于 [object Object]
,并且可以互换使用。因此,设置或引用 [c] 与设置或引用 [b] 是完全相同的。
22、以下代码在控制台中输出的结果为?
console.log((function f(n){return ((n > 1) ? n * f(n - 1) : n)})(10));
该代码将输出 10 阶乘的值(即 10!或 3628800)。
原因如下:
命名函数 f() 以递归的方式调用自身,直到它调用到 f(1),它就会返回 1,这就是这个函数的作用:
f(10) : returns 10 * f(9) ;
f(9) : returns 9 * f(8);
f(8) : returns 8 * f(7);
f(7) : returns 7 * f(6);
f(6) : returns 6 * f(5);
f(5) : returns 5 * f(4);
f(4) : returns 4 * f(3);
f(3) : returns 3 * f(2);
f(2) : returns 2 * f(1);
f(1) : returns 1;
23、观察下面这段代码,控制台将输出什么?为什么?
(function(x) {
return (function (y) {
console.log(x);
})(2)
})(1)
该代码输出的结果将是 1。
原因如下:此代码是一个闭包,闭包是一个函数,其包含创建闭包时在范围内的所有变量或函数。在 JavaScript 中,闭包被定义为“内部函数”,即在另一功能的主体内定义的功能,闭包的一个重要的特征就是内部函数仍然可以访问外部函数的变量。
因此,在这个例子中,由于 x 没有在内部函数中定义,因此回去外部函数的作用域中寻找,外部作用域变量 x 的值为 1,所以最后输出的结果为 1。
24、以下代码将输出的结果是什么?为什么?
var hero = {
_name : 'John Doe',
getSecretIdentity : function(){
return this._name;
};
};
var stoleSecretIdentity = hero.getSecretIdentity;
console.log(stoleSecretIdentity());
console.log(hero.getSecretIdentity());
该代码的输出结果为:
undefined
John Doe
之所以第一个打印出的结果为 undefined 是因为 stoleSecretIdentity 方法是从 hero 对象中提取的方法,如果我们调用这个方法,this 代表的是全局上下文,而在全局上下文中并没有 _name 属性,因此会是 undefined。
我们可以通过如下方式将 this 强行绑定到 hero 上:
var stoleSecretIdentity = hero.getSecretIdentity.bind(hero);
25、创建一个函数,给定页面上的 DOM 元素,将访问元素本身及其所有后代(不仅仅是它的直接子元素)。对于每个访问的元素,函数应该将该元素传递给提供的回调函数。
该函数的参数应该是:
一个 DOM 元素
一个回调函数(以 DOM 元素作为参数)
访问树中的所有元素(DOM)是经典的深度优先搜素算法应用程序,以下是一个事例解决方案:
function Traverse(p_element,p_callback){
p_callback(p_element);
var list = p_element.children;
for(var i = 0 ; i < list.length ; i++){
Traverse(list[i],p_callbanck);
};
};
26、以下代码的输出结果是什么?
var length = 10;
function fn() {
console.log(this.length);
};
var obj = {
length: 5,
method: function(fn){
fn();
arguments[0]();
}
};
obj.method(fn,1);
其输出结果为:
10
2
为什么输出的结果不是 10 和 5 呢?
第一个输出 10 应该很好理解,由于函数 fn 是在全局定义的所以它的 this 也就是作用域是窗口。之后我们在全局定义了 var length = 10 ,其作用域也是窗口,所以在函数 fn 中打印 this.length 即是打印的全局的 length,虽然 fn 函数后面是在内部方法中调用的,但其最初是在全局中定义的,所以它是可以访问全局的 length 的,所以它的输出结果为 10。
至于第二个,首先 obj.method 用参数 fn 和 1 调用。虽然方法只接受一个参数,但调用时是可以传多个参数的,只不过第一个是作为函数回调,其它的仅仅是数字而已。
之后我们还需要知道 argument[] 数组是用来访问 JavaScript 函数中的任意数量的参数,所以 argument0 只不过是在方法中调用 fn 函数,而此时函数的作用域则成为了参数数组,由于参数数组的长度为 2 ,所以最后输出的结果是 2。
27、考虑下面的代码,输出结果是什么?为什么?
(function () {
try {
throw new Error()
}catch(x){
var x = 1,y = 2;
console.log(x);
};
console.log(x);
console.log(y);
})();
1
undefined
2
首先,var 语句会被挂起(没有初始化值)到它所属的全局或函数作用域的顶部,即使它是位于 catch 语句块内;之后 catch(x) 中的 x 是 try 语句中所抛出的错误信息,且只可在 catch 语句块中可见,该函数中报错信息 x 最终被赋值为 1,所以 catch 语句块中打印出来的 x 的值为 1,而外部的 x 现在还是未定义,所以外部的 x打印出来是 undefined ;而 y 不是 try 语句抛出的报错信息,所以 y 在 catch 语句块中定义的值在全局中是可以读取到的,所以最终外部的 y 打印出来的值为 2。用代码表示相当于:
(function () {
var x , y; // 变量提升
try {
throw new Error()
}catch(x){
x = 1; // 内部的 x,指的是 try 内抛出的错误,不是外部的 x
y = 2; // 外部的 y,只有这一个
console.log(x); // 打印的是内部的 x
};
console.log(x);
console.log(y);
})();
1
undefined
2
28、这段代码的输出结果是什么?
var x = 21;
var girl = function() {
console.log(x);
var x = 20;
};
girl();
这段代码的输出结果实际上是 undefined。
因为浏览器中的 JS 解析器实际会执行两个步骤去解析 JS 代码:
1、浏览器会先在代码中寻找 var、function、参数等内容,之后将其提到当前作用域的最前边
2、在做完第一步时,JS 解析器才会执行逐行解读代码
因此,上边的代码就变化为:
var x;
var girl = function() {
console.log(x);
var x = 20;
};
x = 21;
girl();
此时会逐行解析代码,当解析到 girl() 是会调用 girl 函数,此时又形成了一个局部作用域,内部还会继续执行 1、2 步骤,此时代码就会变为:
var x;
var girl = function() {
var x;
console.log(x);
x = 20;
};
x = 21;
girl();
因此,最后输出的结果会是 undefined。
29、你如何克隆一个对象?
var obj = {a:1 , b:2};
var objClone = Object.assign({},obj);
现在 objClone 的值是 {a:1,b:2},但指向与 obj 不同的对象。
Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象,之后返回目标对象。
但是它存在潜在的缺陷:这个方法只是浅拷贝,并不是深拷贝,这就意味着嵌套的对象不会被复制,它们仍然引用与原始相同的嵌套对象,例如:
let obj = {
a: 1,
b: 2,
c: {
age: 30
}
};
var objClone = Object.assign({},obj);
console.log('objClone',objClone); //{a: 1,b: 2,c:{age: 30}}
obj.c.age = 45;
console.log('After Change - obj:',obj.c.age); // 45
console.log('After Change - objclone:',obj.c.age); // 45
30、下面代码将打印出什么?
for(let i = 0 ; i < 5 ; i ++){
setTimeout(function () { conosle.log(i);}, i * 1000);
};
这段代码打印出的结果会是 0、1、2、3、4,因为此处使用的是 let 而不是 var。
31、下面代码将打印出什么?
console.log(1 < 2 < 3);
console.log(3 > 2 > 1);
第一条语句如我们所愿返回了 true。
而第二条语句则会返回 false,原因是引擎会针对 < 和 > 的操作符进行关联性工作,它会从左到右进行比较,因为 3 > 2 是 true,之后会用 true 与 1 进行比较,因为 true 的值可以转化为 1 ,所以 1 > 1 是错误的,所以最后返回 false。
32、如何在数组开头添加元素?如何在数组最后添加元素?
var myarr = ["a","b","c","d"];
myarr.push("end");
myarr.unshift("start");
console.log(myarr); // ["start","a","b","c","d","end"]
还可以使用 ES6 的扩展运算符:
myarr = ["start",...myarr];
myarr = [...myarr,"end"];
或者,简而言之:
myarr = ["start",...myarr,"end"];
33、想象一下你有这样的代码:
var a = [1,2,3];
a、这会导致崩溃吗?
a[10] = 99;
b、这个输出的结果是什么?
console.log(a[6]);
a、它不会崩溃,JavaScript 引擎将使阵列插槽 3 至 9 成为“空插槽”。
b、在这里,a[6] 讲输出 undefined,但实际上仍旧为空,而不是未定义。在某种情况下,这可能是一个重要的细微差别。例如,使用 map() 时,map() 的输出中的空插槽将保持为空,但未定义的插槽将使用传递给它的函数映射:
var b = [undefined];
b[2] = 1;
console.log(b);
console.log(b.map(e => 7));
注:map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果
34、typeof undefined == typeof NULL 的值是什么?
该表达式将输出 true,因为 NULL 将被视为未定义的值。
注:JavaScript 区分大小写,我们这里的 NULL 并不是 null。
35、下面代码将返回什么?
console.log(typeof typeof 1);
这段代码的输出结果将是:string
因为 typeof 1 将返回"number",而 typeof “number” 将返回字符串。
36、以下代码输出什么?为什么?
var b = 1;
function outer(){
var b = 2;
function inner() {
b++;
var b = 3;
console.log(b);
};
inner();
};
outer();
该代码输出的结果将是 3。
在这个例子中有三个闭包,每个都有它自己的 var b 声明。当调用变量时,将按照从本地到全局的顺序检查闭包,直到找到实例。由于内部闭包有自己的变量 b,所以就会直接输出。
此外,由于变量提升,内部的代码将被解析成如下:
function inner() {
var b; // b is undefined
b++; // b is NaN
b = 3; // b is 3
console.log(b); // 3
};