那么你认为你了解 JavaScript 吗?
原始
JavaScript是一门有趣的语言,我们都因为它的本质而热爱它。浏览器是 JavaScript 的家园,两者在我们服务中协同工作。JS 中
有一些概念人们往往轻视,有时甚至会弄混。像原型、闭包和事件循环这样的概念仍然是大多数 JS 开发人员绕道而行的晦涩领域之一。正如我们所知,“一知半解是件坏事”,它可能会导致犯错。
我们来玩一个小游戏吧。我会问你几个问题,你需要尝试全部回答。即使你不知道答案,或者答案超出你的知识范围,也可以猜一下。记下你的答案,然后对照下面的答案。每答对一个问题,给自己打1分。开始吧。
免责声明:我特意在以下部分代码片段中使用了“var”,以便您可以将其复制粘贴到浏览器控制台中,而不会遇到 SyntaxError 错误。
但我并不赞同使用“var”声明变量。使用“let”和“const”声明变量可以使您的代码更健壮,更不容易出错。
问题1:浏览器控制台上会打印什么?
var a = 10;
function foo() {
console.log(a); // ??
var a = 20;
}
foo();
问题 2:如果我们使用 let 或 const 代替 var,输出会相同吗?
var a = 10;
function foo() {
console.log(a); // ??
let a = 20;
}
foo();
问题 3:“newArray”中将包含哪些元素?
var array = [];
for(var i = 0; i <3; i++) {
array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // ??
问题4:如果我们在浏览器控制台中运行'foo'函数,是否会导致堆栈溢出错误?
function foo() {
setTimeout(foo, 0); // will there by any stack overflow error?
};
问题 5:如果我们在控制台中运行以下函数,页面(选项卡)的 UI 是否保持响应?
function foo() {
return Promise.resolve().then(foo);
};
问题 6:我们能否以某种方式对以下语句使用扩展语法而不导致 TypeError?
var obj = { x: 1, y: 2, z: 3 };
[...obj]; // TypeError
问题 7:运行以下代码片段时,控制台上会打印什么?
var obj = { a: 1, b: 2 };
Object.setPrototypeOf(obj, {c: 3});
Object.defineProperty(obj, 'd', { value: 4, enumerable: false });
// what properties will be printed when we run the for-in loop?
for(let prop in obj) {
console.log(prop);
}
问题 8:xGetter() 将打印什么值?
var x = 10;
var foo = {
x: 90,
getX: function() {
return this.x;
}
};
foo.getX(); // prints 90
var xGetter = foo.getX;
xGetter(); // prints ??
答案
现在,让我们尝试从上到下回答每个问题。我会在尝试揭开这些行为神秘面纱的同时,提供一些简要的解释和参考。
答案 1: undefined。
解释:用 var 关键字声明的变量在 JavaScript 中会被提升,并在内存中被赋予undefined的值。但初始化发生在你在代码中输入它们的位置。此外, var 声明的变量是函数作用域的,而let和const是块作用域的。因此,整个过程如下:
var a = 10; // global scope
function foo() {
// Declaration of var a will be hoisted to the top of function.
// Something like: var a;
console.log(a); // prints undefined
// actual initialisation of value 20 only happens here
var a = 20; // local scope
}
答案 2: ReferenceError:a 未定义。
解释: let和const允许声明作用域受限于使用它的块、语句或表达式的变量。与var不同,这些变量不会被提升,并且具有所谓的暂时死区(TDZ)。尝试在TDZ中访问这些变量将引发ReferenceError,因为它们只能在执行到声明处之前访问。阅读更多关于JavaScript 中的词法作用域、执行上下文和堆栈的内容。
var a = 10; // global scope
function foo() { // enter new scope, TDZ starts
// Uninitialised binding for 'a' is created
console.log(a); // ReferenceError
// TDZ ends, 'a' is initialised with value of 20 here only
let a = 20;
}
下表概述了 JavaScript 中使用的不同关键字的提升行为和作用域(来源:Axel Rauschmayer的博客文章)。
Keyword | Hoisting | Scope | Creates global properties | |
---|---|---|---|---|
var | Declaration | Function | Yes | |
let | Temporal dead zone | Block | No | |
const | Temporal dead zone | Block | No | |
function | Complete | Block | Yes | |
class | No | Block | No | |
import | Complete | Module-global | No |
答案 3: [3, 3, 3]。
解释:在for 循环开头使用var关键字声明变量会为该变量创建单一绑定(存储空间)。阅读更多关于闭包的内容。让我们再看一下 for 循环。
// Misunderstanding scope:thinking that block-level scope exist here
var array = [];
for (var i = 0; i < 3; i++) {
// Every 'i' in the bodies of the three arrow functions
// referes to the same binding, which is why they all
// return the same value of '3' at the end of the loop.
array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // [3, 3, 3]
如果您使用 let 声明一个具有块级范围的变量,则会为每次循环迭代创建一个新的绑定。
// Using ES6 block-scoped binding
var array = [];
for (let i = 0; i < 3; i++) {
// This time, each 'i' refers to the binding of one specific iteration
// and preserves the value that was current at that time.
// Therefore, each arrow function returns a different value.
array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // [0, 1, 2]
解决这个问题的另一种方法是使用闭包。
// After understanding static scoping and thus closures.
// Without static scoping, there's no concept of closures.
let array = [];
for (var i = 0; i < 3; i++) {
// invoking the function to capture (closure) the variable's current value in the loop.
array[i] = (function(x) {
return function() {
return x;
};
})(i);
}
const newArray = array.map(el => el());
console.log(newArray); // [0, 1, 2]
答案 4: 不是。
解释:JavaScript 并发模型基于“事件循环”。当我说“浏览器是 JS 的家”时,我真正的意思是浏览器提供了运行时环境来执行我们的 JavaScript 代码。浏览器的主要组件包括调用堆栈、事件循环、任务队列和Web API 。诸如setTimeout、setInterval和Promise之类的全局函数不是 JavaScript 的一部分,而是 Web API 的一部分。JavaScript 环境的可视化表示如下所示:
JS 调用栈遵循后进先出 (LIFO) 的原则。引擎每次从栈中取出一个函数,并按从上到下的顺序运行代码。每当遇到一些异步代码(例如setTimeout )时,它就会将其交给 Web API(箭头 1)。因此,每当触发事件时,回调都会被发送到任务队列(箭头 2)。
事件循环持续监视任务队列,并按照队列顺序逐个处理回调。每当调用堆栈为空时,事件循环就会拾取回调并将其放入堆栈(箭头 3)进行处理。请记住,如果调用堆栈不为空,事件循环将不会将任何回调推送到堆栈。
想要更详细地了解 JavaScript 中事件循环的工作原理,我强烈推荐 Philip Roberts 的这个视频。此外,你还可以通过这个很棒的工具来可视化和理解调用堆栈。现在就运行“foo”函数,看看会发生什么吧!
现在,有了这些知识,让我们尝试回答上述问题:
步骤
- 调用foo()会将foo函数放入调用堆栈。
- 在处理里面的代码时,JS 引擎遇到了setTimeout。
- 然后,它将foo回调交给WebAPI(箭头 1),并从函数返回。调用堆栈再次为空。
- 计时器设置为 0,因此 foo 将被发送到任务队列(箭头 2)。
- 由于我们的调用堆栈是空的,事件循环将选择foo回调并将其推送到调用堆栈进行处理。
- 该过程再次重复,并且堆栈永远不会溢出。
答案 5: 否。
解释:大多数时候,我看到开发人员假设事件循环图中只有一个任务队列。但事实并非如此。我们可以有多个任务队列。浏览器可以选择任何队列并处理其中的回调。
从高层次上讲,JavaScript 中存在宏任务和微任务。setTimeout回调是宏任务,而Promise回调是微任务。它们的主要区别在于执行方式。宏任务在单个循环中一次一个地被推入堆栈,但微任务队列在执行返回事件循环(包括任何额外排队的项目)之前始终会被清空。因此,如果您以处理项目的速度将项目添加到此队列,那么您将永远在处理微任务。如需更深入的解释,请观看Jake Archibald的视频或文章。
在执行返回事件循环之前,微任务队列始终是空的
现在,当您在控制台中运行以下代码片段时:
function foo() {
return Promise.resolve().then(foo);
};
每次调用“foo”都会在微任务队列中继续添加另一个“foo”回调,因此事件循环无法继续处理其他事件(滚动、点击等),直到该队列完全清空。结果,它会阻塞渲染。
答案 6: 是的,通过使对象可迭代。
说明:扩展语法和for-of语句迭代可迭代对象定义的要迭代的数据。数组或 Map 是具有默认迭代行为的内置可迭代对象。对象不是可迭代对象,但你可以使用可迭代协议和迭代器协议使它们可迭代。
在 Mozilla 文档中,如果一个对象实现了 @@iterator 方法,那么它就被称为可迭代的,这意味着该对象(或其原型链上的一个对象)必须具有一个带有 @@iterator 键的属性,该属性可通过常量 Symbol.iterator 获得。
上述语句可能看起来有点冗长,但下面的例子会更有意义:
var obj = { x: 1, y: 2, z: 3 };
obj[Symbol.iterator] = function() {
// An iterator is an object which has a next method,
// which also returns an object with atleast
// one of two properties: value & done.
// returning an iterator object
return {
next: function() {
if (this._countDown === 3) {
return { value: this._countDown, done: true };
}
this._countDown = this._countDown + 1;
return { value: this._countDown, done: false };
},
_countDown: 0
};
};
[...obj]; // will print [1, 2, 3]
您还可以使用生成器函数来自定义对象的迭代行为:
var obj = { x: 1, y: 2, z: 3 };
obj[Symbol.iterator] = function*() {
yield 1;
yield 2;
yield 3;
};
[...obj]; // print [1, 2, 3]
答案 7: a、b、c。
解释:for-in 循环遍历对象本身的可枚举属性以及该对象从其原型继承的属性。可枚举属性是指可以在 for-in 循环中包含并访问的属性。
var obj = { a: 1, b: 2 };
var descriptor = Object.getOwnPropertyDescriptor(obj, "a");
console.log(descriptor.enumerable); // true
console.log(descriptor);
// { value: 1, writable: true, enumerable: true, configurable: true }
现在你有了这些知识,应该很容易理解为什么我们的代码会打印这些特定的属性:
var obj = { a: 1, b: 2 }; // a, b are both enumerables properties
// setting {c: 3} as the prototype of 'obj', and as we know
// for-in loop also iterates over the properties obj inherits
// from its prototype, 'c' will also be visited.
Object.setPrototypeOf(obj, { c: 3 });
// we are defining one more property 'd' into our 'obj', but
// we are setting the 'enumerable' to false. It means 'd' will be ignored.
Object.defineProperty(obj, "d", { value: 4, enumerable: false });
for (let prop in obj) {
console.log(prop);
}
// it will print
// a
// b
// c
答案 8:10 。解释:当我们将x初始化到全局作用域时,它就变成了window对象的属性(假设是浏览器环境,而不是严格模式)。请看下面的代码:
var x = 10; // global scope
var foo = {
x: 90,
getX: function() {
return this.x;
}
};
foo.getX(); // prints 90
let xGetter = foo.getX;
xGetter(); // prints 10
我们可以断言:
window.x === 10; // true
this始终指向调用该方法的对象。因此,在 foo.getX() 的例子中,this指向foo对象,并返回值 90。而在xGetter()的例子中,this指向window对象,并返回值 10。
要检索foo.x的值,我们可以通过使用Function.prototype.bind将this的值绑定到foo对象来创建一个新函数。
let getFooX = foo.getX.bind(foo);
getFooX(); // prints 90
就这样!如果你所有答案都正确,那就恭喜你!我们都是在犯错中学习的。关键在于了解错误背后的“原因”。了解你的工具,并更好地理解它们。如果你喜欢这篇文章,给我点几声❤️,我一定会很高兴😀。
你的分数是多少?
文章来源:https://dev.to/aman_singh/so-you-think-you-know-javascript-5c26