8 个问题看你是否真的懂 JS

论坛 期权论坛 期权     
前端大全   2019-7-28 23:18   4425   0
(给前端大全加星标,提升前端技能)
英文:Amandeep Singh  译文:前端小智
https://juejin.im/post/5d2d146bf265da1b9163c5c9
JavaScript 是一种有趣的语言,我们都喜欢它,因为它的性质。浏览器是JavaScript的主要运行的地方,两者在我们的服务中协同工作。JS有一些概念,人们往往会对它掉以轻心,有时可能会忽略不计。原型、闭包和事件循环等概念仍然是大多数JS开发人员绕道而行的晦涩领域之一。正如我们所知,无知是一件危险的事情,它可能会导致错误。



接下来,来看看几个问题,你也可以试试想想,然后作答。

问题1:浏览器控制台上会打印什么?

  1. var a = 10;
  2. function foo() {
  3.     console.log(a); // ??
  4.     var a = 20;
  5. }
  6. foo();
复制代码
问题2:如果我们使用 let 或 const 代替 var,输出是否相同
  1. var a = 10;
  2. function foo() {
  3.     console.log(a); // ??
  4.     let a = 20;
  5. }
  6. foo();
复制代码

问题3:"newArray"中有哪些元素?

  1. var array = [];
  2. for(var i = 0; i  i);
  3. }
  4. var newArray = array.map(el => el());
  5. console.log(newArray); // ??   
复制代码

问题4:如果我们在浏览器控制台中运行'foo'函数,是否会导致堆栈溢出错误?

  1. function foo() {
  2.   setTimeout(foo, 0); // 是否存在堆栈溢出错误?
  3. };
复制代码

问题5: 如果在控制台中运行以下函数,页面(选项卡)的 UI 是否仍然响应
  1. function foo() {
  2.   return Promise.resolve().then(foo);
  3. };  
复制代码

问题6: 我们能否以某种方式为下面的语句使用展开运算而不导致类型错误

  1. var obj = { x: 1, y: 2, z: 3 };
  2. [...obj]; // TypeError
复制代码

问题7:运行以下代码片段时,控制台上会打印什么?
  1. var obj = { a: 1, b: 2 };
  2. Object.setPrototypeOf(obj, {c: 3});
  3. Object.defineProperty(obj, 'd', { value: 4, enumerable: false });
  4. // what properties will be printed when we run the for-in loop?
  5. for(let prop in obj) {
  6.     console.log(prop);
  7. }
复制代码

问题8:xGetter() 会打印什么值?
  1. var x = 10;
  2. var foo = {
  3.   x: 90,
  4.   getX: function() {
  5.     return this.x;
  6.   }
  7. };
  8. foo.getX(); // prints 90
  9. var xGetter = foo.getX;
  10. xGetter(); // prints ??
复制代码

答案


现在,让我们从头到尾回答每个问题。我将给您一个简短的解释,同时试图揭开这些行为的神秘面纱,并提供一些参考资料。

[h3]问题1:
  1. undefined
复制代码
  1. [/code][/h3][h3]解析:[/h3]使用[code]var
复制代码
关键字声明的变量在JavaScript中会被提升,并在内存中分配值
  1. undefined
复制代码
。但初始化恰发生在你给变量赋值的地方。另外,
  1. var
复制代码
声明的变量是函数作用域的,而
  1. let
复制代码
  1. const
复制代码
是块作用域的。所以,这就是这个过程的样子:
  1. var a = 10; // 全局使用域
  2. function foo() {
  3. // var a 的声明将被提升到到函数的顶部。
  4. // 比如:var a
  5. console.log(a); // 打印 undefined
  6. // 实际初始化值20只发生在这里
  7.    var a = 20; // local scope
  8. }
复制代码
[h3]问题 2:
  1. ReferenceError:
复制代码
  1. a undefined
复制代码
。[/h3][h3]解析:[/h3]
  1. let
复制代码
  1. const
复制代码
声明可以让变量在其作用域上受限于它所使用的块、语句或表达式。与
  1. var
复制代码
不同的是,这些变量没有被提升,并且有一个所谓的暂时死区(TDZ)。试图访问TDZ中的这些变量将引发
  1. ReferenceError
复制代码
,因为只有在执行到达声明时才能访问它们。
  1. var a = 10; // 全局使用域
  2. function foo() { // TDZ 开始
  3. // 创建了未初始化的'a'
  4.     console.log(a); // ReferenceError
  5. // TDZ结束,'a'仅在此处初始化,值为20
  6.     let a = 20;
  7. }
复制代码
下表概述了与JavaScript中使用的不同关键字声明的变量对应的提升行为和使用域:


[h3]问题 3:
  1. [3, 3, 3]
复制代码
  1. [/code][/h3][h3]解析:[/h3]在[code]for
复制代码
循环的头部声明带有
  1. var
复制代码
关键字的变量会为该变量创建单个绑定(存储空间)。阅读更多关于闭包的信息。让我们再看一次for循环。
  1. // 误解作用域:认为存在块级作用域
  2. var array = [];
  3. for (var i = 0; i < 3; i++) {
  4.   // 三个箭头函数体中的每个`'i'`都指向相同的绑定,
  5.   // 这就是为什么它们在循环结束时返回相同的值'3'。
  6.   array.push(() => i);
  7. }
  8. var newArray = array.map(el => el());
  9. console.log(newArray); // [3, 3, 3]
复制代码

如果使用
  1. let
复制代码
声明一个具有块级作用域的变量,则为每个循环迭代创建一个新的绑定。
  1. // 使用ES6块级作用域
  2. var array = [];
  3. for (let i = 0; i < 3; i++) {
  4.   // 这一次,每个'i'指的是一个新的的绑定,并保留当前的值。
  5. // 因此,每个箭头函数返回一个不同的值。
  6.   array.push(() => i);
  7. }
  8. var newArray = array.map(el => el());
  9. console.log(newArray); // [0, 1, 2]
复制代码
解决这个问题的另一种方法是使用闭包。
  1. let array = [];
  2. for (var i = 0; i < 3; i++) {
  3.   array[i] = (function(x) {
  4.     return function() {
  5.       return x;
  6.     };
  7.   })(i);
  8. }
  9. const newArray = array.map(el => el());
  10. console.log(newArray); // [0, 1, 2]
复制代码
[h3]问题4 : 不会溢出[/h3][h3]解析:[/h3]JavaScript并发模型基于“事件循环”。当我们说“浏览器是 JS 的家”时我真正的意思是浏览器提供运行时环境来执行我们的JS代码。
浏览器的主要组件包括调用堆栈,事件循环,任务队列和Web API。像
  1. setTimeout
复制代码
  1. setInterval
复制代码
  1. Promise
复制代码
这样的全局函数不是JavaScript的一部分,而是 Web API 的一部分。JavaScript 环境的可视化形式如下所示:


JS调用栈是后进先出(LIFO)的。引擎每次从堆栈中取出一个函数,然后从上到下依次运行代码。每当它遇到一些异步代码,如
  1. setTimeout
复制代码
,它就把它交给
  1. Web API
复制代码
(箭头1)。因此,每当事件被触发时,
  1. callback
复制代码
都会被发送到任务队列(箭头2)。
事件循环(Event loop)不断地监视任务队列(Task Queue),并按它们排队的顺序一次处理一个回调。每当调用堆栈(call stack)为空时,Event loop获取回调并将其放入堆栈(stack )(箭头3)中进行处理。请记住,如果调用堆栈不是空的,则事件循环不会将任何回调推入堆栈。
现在,有了这些知识,让我们来回答前面提到的问题:
步骤

  • 调用
    1. foo()
    复制代码
    会将
    1. foo
    复制代码
    函数放入调用堆栈(call stack)。
  • 在处理内部代码时,JS引擎遇到
    1. setTimeout
    复制代码

  • 然后将
    1. foo
    复制代码
    回调函数传递给WebAPIs(箭头1)并从函数返回,调用堆栈再次为空
  • 计时器被设置为0,因此
    1. foo
    复制代码
    将被发送到任务队列(箭头2)。
  • 由于调用堆栈是空的,事件循环将选择
    1. foo
    复制代码
    回调并将其推入调用堆栈进行处理。
  • 进程再次重复,堆栈不会溢出。
运行示意图如下所示:


[h3]问题5 : 不会响应[/h3][h3]解析:[/h3]大多数时候,开发人员假设在事件循环图中只有一个任务队列。但事实并非如此,我们可以有多个任务队列。由浏览器选择其中的一个队列并在该队列中处理回调。
在底层来看,JavaScript中有宏任务和微任务。
  1. setTimeout
复制代码
回调是宏任务,而
  1. Promise
复制代码
回调是微任务。
主要的区别在于他们的执行方式。宏任务在单个循环周期中一次一个地推入堆栈,但是微任务队列总是在执行后返回到事件循环之前清空。因此,如果你以处理条目的速度向这个队列添加条目,那么你就永远在处理微任务。只有当微任务队列为空时,事件循环才会重新渲染页面、
现在,当你在控制台中运行以下代码段
  1. function foo() {
  2.   return Promise.resolve().then(foo);
  3. };
复制代码

每次调用'
  1. foo
复制代码
'都会继续在微任务队列上添加另一个'
  1. foo
复制代码
'回调,因此事件循环无法继续处理其他事件(滚动,单击等),直到该队列完全清空为止。因此,它会阻止渲染。
[h3]问题6 : 会导致TypeError错误[/h3][h3]解析:[/h3]展开语法 和 for-of 语句遍历
  1. iterable
复制代码
对象定义要遍历的数据。
  1. Array
复制代码
  1. Map
复制代码
是具有默认迭代行为的内置迭代器。对象不是可迭代的,但是可以通过使用iterable和iterator协议使它们可迭代。
在Mozilla文档中,如果一个对象实现了
  1. @@iterator
复制代码
方法,那么它就是可迭代的,这意味着这个对象(或者它原型链上的一个对象)必须有一个带有
  1. @@iterator
复制代码
键的属性,这个键可以通过常量
  1. Symbol.iterator
复制代码
获得。
上述语句可能看起来有点冗长,但是下面的示例将更有意义:
  1. var obj = { x: 1, y: 2, z: 3 };
  2. obj[Symbol.iterator] = function() {
  3.   // iterator 是一个具有 next 方法的对象,
  4.   // 它的返回至少有一个对象
  5.   // 两个属性:value&done。
  6.   // 返回一个 iterator 对象
  7.   return {
  8.     next: function() {
  9.       if (this._countDown === 3) {
  10.         const lastValue = this._countDown;
  11.         return { value: this._countDown, done: true };
  12.       }
  13.       this._countDown = this._countDown + 1;
  14.       return { value: this._countDown, done: false };
  15.     },
  16.     _countDown: 0
  17.   };
  18. };
  19. [...obj]; // 打印 [1, 2, 3]
复制代码

还可以使用 generator 函数来定制对象的迭代行为:
  1. var obj = {x:1, y:2, z: 3}
  2. obj[Symbol.iterator] = function*() {
  3.   yield 1;
  4.   yield 2;
  5.   yield 3;
  6. }
  7. [...obj]; // 打印 [1, 2, 3]
复制代码
[h3]问题7 : a, b, c[/h3][h3]解析:[/h3]
  1. for-in
复制代码
循环遍历对象本身的可枚举属性以及对象从其原型继承的属性。可枚举属性是可以在
  1. for-in
复制代码
循环期间包含和访问的属性。
  1. var obj = { a: 1, b: 2 };
  2. var descriptor = Object.getOwnPropertyDescriptor(obj, "a");
  3. console.log(descriptor.enumerable); // true
  4. console.log(descriptor);
  5. // { value: 1, writable: true, enumerable: true, configurable: true }
复制代码

现在你已经掌握了这些知识,应该很容易理解为什么我们的代码要打印这些特定的属性
  1. var obj = { a: 1, b: 2 }; //a,b 都是 enumerables 属性
  2. // 将{c:3}设置为'obj'的原型,并且我们知道
  3. // for-in 循环也迭代 obj 继承的属性
  4. // 从它的原型,'c'也可以被访问。
  5. Object.setPrototypeOf(obj, { c: 3 });
  6. // 我们在'obj'中定义了另外一个属性'd',但是
  7. // 将'enumerable'设置为false。 这意味着'd'将被忽略。
  8. Object.defineProperty(obj, "d", { value: 4, enumerable: false });
  9. for (let prop in obj) {
  10.   console.log(prop);
  11. }
  12. // 打印
  13. // a
  14. // b
  15. // c
复制代码
[h3]问题8 : 10[/h3][h3]解析:[/h3]在全局范围内初始化
  1. x
复制代码
时,它成为window对象的属性(不是严格的模式)。看看下面的代码:
  1. var x = 10; // global scope
  2. var foo = {
  3.   x: 90,
  4.   getX: function() {
  5.     return this.x;
  6.   }
  7. };
  8. foo.getX(); // prints 90
  9. let xGetter = foo.getX;
  10. xGetter(); // prints 10
复制代码
咱们可以断言:

  1. window.x === 10; // true
复制代码
  1. this
复制代码
始终指向调用方法的对象。因此,在
  1. foo.getx()
复制代码
的例子中,它指向
  1. foo
复制代码
对象,返回
  1. 90
复制代码
的值。而在
  1. xGetter()
复制代码
的情况下,
  1. this
复制代码
指向 window对象, 返回 window 中的
  1. x
复制代码
的值,即
  1. 10
复制代码

要获取
  1. foo.x
复制代码
的值,可以通过使用
  1. Function.prototype.bind
复制代码
  1. this
复制代码
的值绑定到
  1. foo
复制代码
对象来创建新函数。
  1. let getFooX = foo.getX.bind(foo);
  2. getFooX(); // 90
复制代码
就这样!如果你的所有答案都正确,那么干得漂亮。咱们都是通过犯错来学习的。这一切都是为了了解背后的“原因”。


推荐阅读
(点击标题可跳转阅读)
浏览器缓存机制全攻略
做 1 个静态页面要价 12 万,有错吗?
拆解 JavaScript 中的异步模式



觉得本文对你有帮助?请分享给更多人
关注「前端大全」加星标,提升前端技能

好文章,我在看
分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:195
帖子:39
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP