前言
本篇文章的题目都来自于以下网站:
https://www.toptal.com/javascript/interview-questions
这几天我看了网站内的题目,然后选了网站内的一些题目翻成中文,并参考网站内原本的解析再加上自己的一些理解,就诞生了这篇文章哈哈~
可以自己先试着回答问题,再往下看答案,假如有看到我写错的地方,非常欢迎告诉我,感谢你~
题目1
(function(){ var a = b = 3;})();console.log("a defined? " + (typeof a !== 'undefined'));console.log("b defined? " + (typeof b !== 'undefined'));
问以上程式码执行结果为何?
答案:a defined? false / b defined? true
原因:var a = b = 3;
代表:
b = 3;var a = b;
在非严格模式下,假如变数未做宣告就赋值,则都为全域变数,以下都是全域变数
mom = '老妈';(function () { mom = '老妈';})();
因此 b = 3,defined == true
而 a 为区域变数,在 console 时已经出去作用域範围,因此为 undefined
题目2
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 = barouter func: self.foo = barinner func: this.foo = undefinedinner func: self.foo = bar
原因:
在 outer 函式,this 和 self 都指向 myObject,因此输出都是 bar,但在 inner 函式,由于 IIFE 有自己的作用域,因此 this 输出为 undefined,但是 self 因为保存了 this 的指向,因此输出为 bar
题目3
function foo1(){ return { bar: "hello" };}function foo2(){ return { bar: "hello" };}console.log(foo1());console.log(foo2());
问以上程式码执行结果为何?
答案:
foo1() 回传 Object {bar: "hello"}
foo2() 回传 undefined
原因:return
如果后面没有任何东西(紧接着换行),会自动补上;
,因此foo2() 才会回传 undefined
补充:
break
和continue
也一样会自动补上;
题目4
写一个方法 isInterger(x),用来判断一个变数是否是整数
答案:
用这个写法会在 x 为很大值出错
function isInteger(x) { return parseInt(x, 10) === x; }
正解:
function isInteger(x) { return Math.round(x) === x; }
用Math.ceil()
或Math.floor()
也可以
题目5
写一个按照下面方式调用都能正常工作的 sum 方法
console.log(sum(2,3)); // Outputs 5console.log(sum(2)(3)); // Outputs 5
答案:
方法1: 使用 arguments
function sum(x) { if(arguments.length == 2) { return arguments[0] + arguments[1]; } else { return (y) => x + y; }}
方法2: 假如呼叫函式时带入的参数小于函式内设定的参数,则那些参数会设定 undefined
function sum(x, y) { if (y !== undefined) { return x + y; } else { return (y) => x + y; }}
题目6
for (var i = 0; i < 5; i++) { var btn = document.createElement('button'); btn.appendChild(document.createTextNode('Button ' + i)); btn.addEventListener('click', function(){ console.log(i); }); document.body.appendChild(btn);}// 执行上面JS后出现5个按钮:<button>Button 0</button><button>Button 1</button><button>Button 2</button><button>Button 3</button><button>Button 4</button>
问题1: 点击按钮4,会印出什么?
问题2: 要怎么样修正才会让点击按钮后出现按钮的数字?
问题1答案:
5,点击其它按钮也都是5
事件监听概念和 setTimeout 差不多,非同步行为要等到同步行为完成后才开始运作,因此当 for 迴圈执行完 i 值是5,不管怎么点都会印出5
问题2答案:
方法1: 使用 IIFE 将每次迴圈的 i 值保存,并把 i 值传给 j ,就能正常运行了
for (var i = 0; i < 5; i++) { var btn = document.createElement('button'); btn.appendChild(document.createTextNode('Button ' + i)); btn.addEventListener('click', (function(j) { return function() { console.log(j); }; })(i)); document.body.appendChild(btn);}
方法2: 或是将整个监听事件放入 IIFE
for (var i = 0; i < 5; i++) { var btn = document.createElement('button'); btn.appendChild(document.createTextNode('Button ' + i)); (function (j) { btn.addEventListener('click', function() { console.log(j); }); })(i); document.body.appendChild(btn);}
方法3: 使用 forEach,使用阵列索引当 i
['a', 'b', 'c', 'd', 'e'].forEach(function (value, i) { var btn = document.createElement('button'); btn.appendChild(document.createTextNode('Button ' + i)); btn.addEventListener('click', function() { console.log(i); }); document.body.appendChild(btn);});
方法4: 把 var 改成 let,利用的是 let 区块作用域({}
)的特性,var 在 for 运作完后还存在全域,但 let 在全域就消失,只有进入 for 才会重新产生 i 并给值
for (let i = 0; i < 5; i++) { var btn = document.createElement('button'); btn.appendChild(document.createTextNode('Button ' + i)); btn.addEventListener('click', function(){ console.log(i); }); document.body.appendChild(btn);}
附图:
题目7
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));
问以上程式码执行结果为何?
答案:
array 1: length=5 last=j,o,n,e,sarray 2: length=5 last=j,o,n,e,s
原因:
使用 reverse(),会改变原本的阵列,并回传阵列 =>arr1, arr2 两个都变成["n", "h", "o", "j"]
因为 reverse() 回传的是原本阵列的 reference,因此之后产生的阵列(arr2)和原本阵列(arr1)都指向同一个记忆体位置在修改 arr2 的时候,同时也修改到 arr1 ,因为实际上是修改它们记忆体内的东西。=>arr1, arr2 两个都变成["n", "h", "o", "j", ["j", "o", "n", "e", "s"]]
(二维阵列)slice(-1)
代表的是拷贝阵列最后一个元素,并进行回传,因此取到的是["j", "o", "n", "e", "s"]
,因此arr1.slice(-1)
和arr2.slice(-1)
都是["j", "o", "n", "e", "s"]
题目8
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);
问以上程式码执行结果为何?
答案:
1223202112NaN2NaN
原因:console.log(1 + "2" + "2");
: 最开头的1会被转成字串连接console.log(1 + +"2" + "2");
: +"2"
会转为 数字正2,和1相加后变成3,再转成字串和"2"
连接console.log(1 + -"1" + "2");
: -"2"
会转为 数字负2,和1相加后变成0,再转成字串和"2"
连接console.log(+"1" + "1" + "2");
: +"1"
会转为 数字正1,但后面是字串所以又被转成字串连接console.log( "A" - "B" + "2");
: 因为"A" - "B"
根本不能减,因此输出 NaN,代表非数字,然后再和"2"
连接console.log( "A" - "B" + 2);
: NaN 和 数字相加结果还是 NaN
题目9
var list = readHugeList();var nextListItem = function() { var item = list.pop(); if (item) { nextListItem(); }};
假如 list 阵列太大,将会导致 stack overflow,如何在不改变递迴的前提下修改这段程式?
答案:
使用 setTimeout,因为 setTimeout 这种非同步行为会放到 task queue(或叫event queue),然后搭配 event loop 一次只放一个递迴的呼叫到 call stack,就解决在 call stack 会 stack overflow 的问题。
var list = readHugeList();var nextListItem = function() { var item = list.pop(); if (item) { setTimeout( nextListItem, 0); }};
题目10
var a={}, b={key:'b'}, c={key:'c'};a[b]=123;a[c]=456;console.log(a[b]);
问以上程式码执行结果为何?
答案:
456
原因:
在 JS 中,物件内的 key 会被隐性转为字串,b 物件加入 a 物件时变成"[object Object]"
,因此在a[b]=123;
这行,a 变成:
{ "[object Object]":123}
而在这行a[c]=456;
,就把 a 物件变成:
{ "[object Object]":456}
故输出为456
题目11
var hero = { _name: 'John Doe', getSecretIdentity: function (){ return this._name; }};var stoleSecretIdentity = hero.getSecretIdentity;console.log(stoleSecretIdentity());console.log(hero.getSecretIdentity());
问以上程式码执行结果为何?假如有出错,该如何修复?
答案:
undefinedJohn Doe
原因:
因为呼叫stoleSecretIdentity()
的时候,getSecretIdentity()
函式从 hero 物件拿出,为全域的状态,因此 this 指向全域,找不到_name
导致印出 undefined
如何修复:
使用 var stoleSecretIdentity = hero.getSecretIdentity.bind(hero);
修复,bind() 会建立一个新函式,而这个函式的 this 会指向 bind() 里的第一个参数
题目12
var length = 10;function fn() {console.log(this.length);}var obj = { length: 5, method: function(fn) { fn(); arguments[0](); }};obj.method(fn, 1);
问以上程式码执行结果为何?
答案:
102
原因:
在 method() 呼叫时,该方法的fun()
印出10,因为 this 指向全域,而arguments[0]();
,我们可以把 arguments 这个类阵列看成arguments = [fn(), 1];
,这时fn()
的 this 指向这个阵列,因此最后输出为2,因为arguments.length
为2
题目13
(function () { try { throw new Error(); } catch (x) { var x = 1, y = 2; console.log(x); } console.log(x); console.log(y);})();
问以上程式码执行结果为何?
答案:
1undefined2
原因:
这段程式码可以看成:
(function () { var x, y; try { throw new Error(); } catch (x) { x = 1; y = 2; console.log(x); } console.log(x); console.log(y);})();
由于 hoisting,x 和 y 出现在函式最上面,然后catch (x)
的 x 是 Error 物件,而后被赋值为1,因此 catch 里的 console 印出1
题目14
var x = 21;var girl = function () { console.log(x); var x = 20;};girl ();
问以上程式码执行结果为何?
答案: undefined
原因:
根据 hoisting 的特性,这段程式码可看成:
var x = 21;var girl = function () { var x; console.log(x); x = 20;};girl ();
一看就了解了,不多解释
题目15
要如何複製一个物件?
答案:
var obj = {a: 1 ,b: 2}var objclone = Object.assign({},obj);
不过使用Object.assign
无法做到完全的深拷贝,在複製后的新物件里面的物件还是和原本物件里的物件指向相同记忆体位置。
let obj = { a: 1, b: 2, c: { age: 30 }};var objclone = Object.assign({},obj);console.log('objclone: ', objclone);obj.c.age = 45;console.log('After Change - obj: ', obj); // 45console.log('After Change - objclone: ', objclone); // 45
题目16
console.log(1 < 2 < 3);console.log(3 > 2 > 1);
问以上程式码执行结果为何?
答案:
truefalse
原因:<
和>
两个运算符为由左到右计算
1 < 2 为 true,然后 true < 3,true == 1,因此结果是 true
3 > 2 为 true, 然后 true > 1,true == 1 ,因此结果是 false
题目17
console.log(typeof typeof 1);
问以上程式码执行结果为何?
答案:
string
原因:
typeof 1 返回 "number" 然后 typeof "number" 返回 string