关于前端:面试官经常问的十个棘手的-JavaScript-问题

30次阅读

共计 5643 个字符,预计需要花费 15 分钟才能阅读完成。

1. 可变性

在 JavaScript 中有七种根本数据类型(stringnumberbooleanundefinedsymbolbigintnull),这些都是不可变的。这意味着一旦调配了一个值,咱们就无奈批改它们,咱们能够做的是将它重新分配给一个不同的值(不同的内存指针)。另一方面,其余数据类型(如 Object 和 Function)是可变的,这意味着咱们能够批改同一内存指针中的值。

// Q1
let text = 'abcde'
text[1] = 'z'
console.log(text) // abcde

字符串是不可变的,因而一旦调配给一个值,就不能将其更改为不同的值,您能够做的是重新分配它。请记住,更改值和重新分配给另一个值是不同的。

// Q2
const arr = [1, 2, 3]
arr.length = 0
console.log(arr) // []

调配 arr.length 为 0 与重置或革除数组雷同,因而此时数组将变为空数组。

// Q3
const arr = [1, 2, 3, 4]
arr[100] = undefined
console.log(arr, arr.length) // [1, 2, 3, 4, empty × 96, undefined] 101

因为数组占用的是间断的内存地位,所以当咱们将索引 100 赋给一个值(包含 undefined)时,JavaScript 会保留索引 0 到 索引 100 的内存,这意味着当初的数组长度为 101。

2. var 和 晋升

// Q4
var variable = 10;
(() => {
  variable2 = 100
  console.log(variable) 
  console.log(variable2)
  variable = 20
  var variable2 = 50
  console.log(variable)
})();
console.log(variable)
var variable = 30
console.log(variable2)
// 10 
// 100 
// 20 
// 20 
// ReferenceError: variable2 is not defined

var 是函数作用域变量,而 letconst 是块级作用域变量,只有 var 能被晋升,这意味着变量申明总是被挪动到顶部。因为晋升,您甚至能够在应用 var 关键字申明变量之前调配、调用或应用该变量。

letconst 不能被晋升,因为它启用了 TDZ(临时性死区),这意味着变量在申明之前是不可拜访的。

在下面的示例中,variable2 在函数外部申明,var 关键字使该变量仅在函数范畴内可用。所以当函数外的任何货色想要应用或者调用该变量时,referenceError 就会被抛出。

// Q5 
test() // 不报错
function test() {cconsole.log('test')
}
test2() // 报错
var test2 = () => console.log('test2')

function 关键字申明的函数能够晋升函数语句,然而不能晋升箭头函数,即便它是应用 var 进行变量申明的。

3. 必然性全局变量

// Q6 
function foo() {
  let a = b = 0;
  a++;
  return a;
}
foo();
typeof b; // number
typeof a; // undefined

console.log(a) // error: ReferenceError: a is not defined

var 是函数作用域,let 是块级作用域变量。尽管看起来 ab 都是应用 let 申明的(let a = b = 0),但实际上变量 b 被申明为全局变量并调配给 Window 对象。换句话说,它相似于:

function foo() {
  window.b = 0;
  let a = b;
  a++;
}

4. 闭包

// Q7
const length = 4;
const fns = [];
const fns2 = [];
for(var i = 0; i < length; i++) {fns.push(() => console.log(i));
}
for(let i = 0; i < length; i++) {fns2.push(() => console.log(i));
}
fns.forEach(fn => fn()); // 4 4 4 4
fns2.forEach(fn => fn()); // 0 1 2 3

闭包是对变量环境的一种爱护,即便变量曾经更改或者已被垃圾回收。在下面的问题中,区别在于变量申明,其中第一个循环应用的是 var,第二个循环应用的是 let

var 是函数作用域变量,因而当它在 for 循环块内申明时,var 被视为全局变量而不是外部变量。另一方面,let 是块级作用域的变量,相似于 Java 和 C++ 等其余语言中的变量申明。

在这种状况下,闭包只产生在 let 变量中,推送到 fns2 数组的每个函数都会记住变量以后的值,无论变量未来是否更改。相同,fns 不记住变量的以后值,它应用全局变量的将来或最终值。

5. 对象

// Q8
var obj1 = {n: 1}
var obj2 = obj1
obj2.n = 2
console.log(obj1) // {n: 2}

// Q9
function foo(obj) {
  obj.n = 3
  obj.name = '测试'
}
foo(obj2)
console,log(obj1) // {n: 3, name: '测试'}

正如咱们所知,对象变量仅蕴含该对象的内存地位指针,所以这里 obj2obj1 指向同一个对象。这意味着如果咱们更改 obj2 的任何值,obj1 也会受到影响,因为实质上它们是同一个对象。同样,当咱们在函数中将对象作为参数传递时,传递的参数只蕴含对象指针。因而,函数能够间接批改对象而不返回任何内容,这种技术称为 通过援用传递

// Q10
var foo = {n: 1};
var bar = foo;
console.log(foo === bar); // true
foo.x = foo = {n: 2};

console.log(foo) // {n: 2}
console.log(bar) // {n: 1, x: { n: 2} }
console.log(foo === bar) // false

因为对象变量只蕴含该对象内存地位的指针,所以当咱们申明 var bar = foo 时,foobar 都指向同一个对象。

在下一个逻辑中,foo = {n: 2} 首先运行,其中 foo 被调配给不同对象,因而 foo 有一个指向不同对象的指针。同时,foo.x = foo 正在运行,这里的 foo 依然蕴含旧指针,所以逻辑相似于:

foo = {n: 2}
bar.x = foo

所以 bar.x = {n: 2},最初 foo 的值是 {n: 2},而 bar{n: 1, x: { n: 2} }

6. this

// Q11
const obj = {
  name: "test",
  prop: {
    name: "prop name",
    print: function(){console.log(this.name) 
    },
  },
  print: function(){console.log(this.name) 
  }
  print2: () => console.log(this.name, this)
}

obj.print() // test
obj.prop.print() // prop name
obj.print2() // undefined, window global object

下面的例子展现了 this 关键字在一个对象中是如何工作的,this 援用执行函数中的执行上下文对象。然而,this 范畴仅在一般函数申明中可用,在箭头函数中不可用。

下面的例子展现了显示绑定,例如在 object1.object2.object3.object4.print() 中,print 函数将应用最新的对象 object4 作为 this 上下文,如果 this 未绑定对象,它将回退到根对象,该对象是在调用 obj.print2() 时的 Window 全局对象。

另一方面,您还必须了解对象上下文之前曾经绑定的隐式绑定,因而下一个函数执行始终应用该对象作为 this 上下文。例如:当咱们应用 func.bind(<object>) 时,它将返回一个 <object> 用作新执行上下文的新函数。

7. 强制转换

// Q12
console.log(1 + "2" + "2"); // 122
console.log(1 + +"2" + "2"); // 32
console.log(1 + -"1" + "2"); // 02
console.log(+"1" + "1" + "2"); // 112
console.log("A" - "B" + "2"); // NaN2
console.log("A" - "B" + 2); // NaN
"10,11" == [[[[10]], 11]] // true (10,11 == 10,11)
"[object Object]" == {name: "test"} true

强制转换是最辣手的 JavaScript 问题之一。一般来说有两条准则,第一条是,如果 2 个操作数与 + 操作符连贯,则两个操作数将首先应用 toString 办法转变为字符串,而后连贯。同时,其余运算符(如 -*/)会将操作数更改为数字,如果它不能被强制转换为一个数字,则返回 NAN

如果操作数蕴含一个对象或数组,那就更辣手了。任何对象的 toString 办法返回的都是 "[object Object]",但在数组中,该 toString 办法将返回由逗号分隔的根底值。

留神:== 示意容许强制转换,而 === 不容许。

8. 异步

// Q13
console.log(1);
new Promise(resolve => {console.log(2);
  return setTimeout(() => {console.log(3);
    resolve();}, 0)
})
setTimeout(function() {console.log(4) }, 1000);
setTimeout(function() {console.log(5) }, 0);
console.log(6);
// 1
// 2
// 6
// 3
// 5
// 4

在这里,你须要晓得事件循环、宏工作和微工作队列是如何工作的。您能够在此处查看这篇文章,这里深入探讨了这些概念。个别状况下,异步函数在所用同步函数执行完后才执行。

// Q14
async function foo() {return 10;}
console.log(foo()) // Promise{<fulfilled>: 10}

一旦函数申明为 async,它总是返回一个 Promise,无论外部逻辑是同步的还异步的。

// Q15
const delay = async (item) => new Promise(resolve => setTimeout(() => {console.log(item);
    resolve(item);
  }, Math.random() * 100)
)
console.log(1)
let arr = [3, 4, 5, 6]
arr.forEach(async item => await delay(item)))
console.log(2)

forEach 函数总是同步的,不论每个循环是同步的还是异步的,这意味着每个循环都不会期待另一个。如果要顺次执行每个循环并互相期待,能够改用 for of

9. 函数

// Q16
if(function f(){}) {console.log(f)
}
// error: ReferenceError: f is not defined

在下面的例子中,if 条件被满足,因为函数申明被认为是一个真值。然而,外部块无法访问函数申明,因为它们具备不同的块作用域。

// Q17
function foo() {
  return 
  {name: 2}
}
foo() // 返回 undefined

因为主动分号插入(ASI)机制,return 语句将以分号完结,并且分号上面的所有内容都不会运行。

// Q18
function foo(a, b, a) {return a + b}
console.log(foo(1, 2, 3)) // 3+2 = 5

function foo2(a, b, c = a) {return a + b + c}
console.log(foo(1, 2)) // 1+2+1 = 4

function foo3(a = b, b) {return a + b}
console.log(foo3(1, 2)) // 1+2 = 3 
console.log(foo3(undefined, 2)) // 谬误

前三次执行的很分明,然而最初一个函数执行会报错,因为 b 在申明之前就被应用了,相似于这样:

let a = b;
let b = 2;

10. 原型

// Q19
function Persion() {}
Persion.prototype.walk = function() {return this}
Persion.run = function() {return this} 

let user = new Persion();
let walk = user.walk;
console.log(walk()) // window object
console.log(user.walk()) // user object
let run = Persion.run;
console.log(run()); // window object
console.log(user.run()); // TypeError: user.run is not a function

原型是存在于每个变量中的对象,用于从其父对象继承个性。例如,当您申明一个字符串变量时,该字符串变量具备一个继承自 String.prototype 的原型,这就是为什么您能够在字符串变量中调用字符串办法的起因,例如 string.replace(), string.substring() 等。

在下面的示例中,咱们将 walk 函数调配给 Persion 函数的原型,并将 run 函数调配给函数对象。这是两个不同的对象,函数应用 new 关键字创立的每个对象都将从函数原型而不是函数对象上继承办法。然而请记住,如果咱们将该函数调配给一个变量,如 let walk = user.walk,该函数将遗记应用 user 作为执行上下文,而是返回到 Window 对象上。

原文:https://medium.com/@andreassu…

正文完
 0