典型的 JavaScript 面试练习(解释)

2025-05-28

典型的 JavaScript 面试练习(解释)

几周前,我在推特上发现了一篇非常有趣的博文:《最佳前端 JavaScript 面试题(由前端工程师撰写)》,作者是 Boris Cherny。

正如你可能猜到的那样,作者展示了一些在面试中可以问的有趣问题。这些问题分为四个部分:概念、编码、调试和系统设计。这里,我将重点介绍调试部分。

我真的很喜欢这些问题,因为它们涉及 JavaScript 的特殊性:对象比较、事件循环、范围、this、原型继承以及与抽象相等比较算法相结合的相等运算符

在阅读解决方案之前,我建议您自己寻找答案。

练习 1

我希望此代码注销“嘿艾米”,但它注销“嘿阿诺德” - 为什么?

function greet (person) {
  if (person == { name: 'amy' }) {
    return 'hey amy'
  } else {
    return 'hey arnold'
  }
}
greet({ name: 'amy' })
Enter fullscreen mode Exit fullscreen mode

回答

问题如下:{ name: 'amy' } != { name: 'amy' }当比较两个对象是否相等或严格相等时,JavaScript 会比较相关的内部引用。此时,这两个对象具有相同的属性和相同的值。但在内存中,这是两个不同的对象。

这里的解决方案可能是:

function greet (person) {
  if (person.name === 'amy') {
    return 'hey amy'
  }
  return 'hey arnold'
}
greet({ name: 'amy' }) // "hey amy"
Enter fullscreen mode Exit fullscreen mode

练习 2

我希望此代码按顺序输出数字 0、1、2、3,但它并没有按照我的预期执行(这是一个你偶尔会遇到的错误,有些人喜欢在面试中询问它)。

for (var i = 0; i < 4; i++) {
  setTimeout(() => console.log(i), 0)
}
Enter fullscreen mode Exit fullscreen mode

问题

我喜欢这个,因为它有点棘手,它处理范围和 JavaScript 事件循环。

这里的典型陷阱是零延迟setTimeout(callback, 0)并不意味着回调将在零毫秒后触发。

以下是事件循环方面发生的情况:

  1. 当前调用堆栈设置为第一个 setTimeout()。
  2. windows.setTimeout() 被视为 Web API(为了实现更好的非阻塞 I/O)。因此,调用栈会将这部分代码发送到正确的 Web API。0 毫秒后,回调(此处为匿名函数)将被发送到队列(而不是调用栈)。
  3. 由于调用堆栈是空闲的,for 循环可以继续到第二个 setTimeout...(在满足此条件 i < 4 后重复)...
  4. 现在循环结束了i === 4。JS 现在可以逐个执行回调队列了。每次 console.log(i) 都会打印 4。

你是否感到迷茫?希望这个动画能更好地帮助你!

用 Loupe 制作的动画(试试看很有趣!)

第二个问题与作用域有关。setTimeout 函数的 4 个实例共享同一个 实例i

var foo = 'bim'
//                                â–¼ this is a reference to variable foo, not his associated value ('bim') 
var getFoo = function () { return foo }
foo = 'boum'

getFoo() // 'boum'
Enter fullscreen mode Exit fullscreen mode

回答

因此,有几种可用的解决方案:

  • 使用立即调用函数表达式 IIFE)。“包装函数”定义后会立即运行。
for (let i = 0; i < 4; i++) {
  (function (i) {
    setTimeout(() => console.log(i), 0)
  })(i)
}
Enter fullscreen mode Exit fullscreen mode
  • 切换到let关键字(而不是var)。这个(新的?)关键字使作用域更容易理解。
for (let i = 0; i < 4; i++) {
  setTimeout(() => console.log(i), 0)
}
Enter fullscreen mode Exit fullscreen mode

练习 3

我希望此代码注销“doggo”,但它注销未定义!

let dog = {
  name: 'doggo',
  sayName () {
    console.log(this.name)
  }
}
let sayName = dog.sayName
sayName()
Enter fullscreen mode Exit fullscreen mode

回答

前面的代码返回undefined。为什么?因为在第一个 let 条件中,我们定义了一个具有两个属性(name 和 sayName() 函数)的对象。然后在第二个 let 中,我们将属性 sayName(一个函数)复制到另一个变量中。然后,我们在它的上下文之外(全局变量中)调用这个变量。sayName() 函数将返回 window.name(如果环境是 Node,则返回全局变量)。然后typeof window.name === "undefined"

  • ðŸ'Ž(脏的那个)。如果我们想保留 sayName 变量,那么我们需要将 dog 的上下文绑定到它上面:
sayName.bind(dog)()
// or:
dog.sayName.bind(dog)()
Enter fullscreen mode Exit fullscreen mode

这很脏吧? ðŸ¤

  • 直接在原始上下文中调用该函数
let dog = {
  name: 'doggo',
  sayName () {
    console.log(this.name)
  }
}
dog.sayName() // will log "doggo"
Enter fullscreen mode Exit fullscreen mode

练习 4

我想让我的狗叫()但结果却报错。为什么?

function Dog (name) {
  this.name = name
}
Dog.bark = function () {
  console.log(this.name + ' says woof')
}
let fido = new Dog('fido')
fido.bark()
Enter fullscreen mode Exit fullscreen mode

回答

我们得到了以下错误TypeError: fido.bark is not a function。在前面的代码中,我们将 bark 函数赋值给了另一个函数 ( Dog()),而这个函数也是一个构造函数。这有可能吗?因为在 JavaScript 中,函数就是对象。

2个解决方案:

  • ðŸ'Ž(脏的那个)。fido.bark 不是一个函数,但Dog.bark它是函数。所以让我们使用这个,并this使用 function.prototype.bind() 来解决这个问题,就像上面的练习一样:
var boundedBark = Dog.bark.bind(fido)
boundedBark() // "fido says woof"
Enter fullscreen mode Exit fullscreen mode

但从我的角度来看,使用 function.prototype.bind() (几乎总是)会导致混乱。

  • ðŸ' 在 Dog 的原型上设置 bark()
function Dog (name) {
  this.name = name
}

Dog.prototype.bark = function () {
  console.log(this.name + ' says woof')
}

let fido = new Dog('fido')
fido.bark() // "fido says woof"
Enter fullscreen mode Exit fullscreen mode

我们还可以使用 class 关键字(ES2015),它只是前面代码的语法糖。

class Dog {
  constructor (name) {
    this.name = name
  }

  bark () {
    console.log(this.name + ' says woof')
  }
}

let fido = new Dog('fido')
fido.bark() // "fido says woof"
Enter fullscreen mode Exit fullscreen mode

练习 5

为什么此代码会返回这样的结果?

function isBig (thing) {
  if (thing == 0 || thing == 1 || thing == 2) {
    return false
  }
  return true
}
isBig(1)    // false
isBig([2])  // false
isBig([3])  // true
Enter fullscreen mode Exit fullscreen mode

回答

这里我们使用简单的相等运算符(例如==),而不是严格的比较运算符(例如===)。使用此运算符,不必比较相同类型。

  • isBig(1)按预期满足条件thing == 1
  • isBig([2])将满足条件thing == 2。当将数组与数字进行比较时,数组将转换为数字。这是抽象相等比较算法的一部分。根据此算法,如果我们将数字与对象(提醒:在 JS 中数组是对象)进行比较,则此数组将转换为数组。这里,所以里面只有一个项[2] == 2

因为这个算法对于大多数普通开发人员来说是晦涩难懂的,所以我们应该避免使用这个运算符(ESLint eqeqeq 规则是你的朋友ðŸ')。

// weird results
[] == ![]     // true
[] == false   // true

// Non transitive relation
"1" == true   // true
"01" == true  // true
"01" == "1"   // false
Enter fullscreen mode Exit fullscreen mode

练习 6(奖励)

如何保持我的英雄列表的不变性?

const heroes = [
  { name: 'Wolverine',      family: 'Marvel',    isEvil: false },
  { name: 'Deadpool',       family: 'Marvel',    isEvil: false },
  { name: 'Magneto',        family: 'Marvel',    isEvil: true  },
  { name: 'Charles Xavier', family: 'Marvel',    isEvil: false },
  { name: 'Batman',         family: 'DC Comics', isEvil: false },
  { name: 'Harley Quinn',   family: 'DC Comics', isEvil: true  },
  { name: 'Legolas',        family: 'Tolkien',   isEvil: false },
  { name: 'Gandalf',        family: 'Tolkien',   isEvil: false },
  { name: 'Saruman',        family: 'Tolkien',   isEvil: true  }
]

const newHeroes = heroes.map(h => {
  h.name = h.name.toUpperCase()
  return h
})
Enter fullscreen mode Exit fullscreen mode

你有什么想法吗?🙂

文章来源:https://dev.to/maxpou/典型-javascript-interview-exercises-explained
PREV
我希望拥有的测试介绍
NEXT
我作为开发人员在日常工作中使用的 Linux 命令和技巧 端口转发 在 VIM 中编辑文件(无需 sudo),但使用 sudo 保存 在终端 ll 中转到行首/行尾 执行您过去执行的命令 同意所有内容 在后台运行一个持久进程并关闭终端 检查谁偷了您最喜欢的端口 读取日志 排序过程 每 X 秒执行一次命令 安静模式 为 crontab 执行的脚本创建日志文件