放在前面,本文原文的标题是 So you think you know JavaScript?javascript
在下感受有些标题党了,不过看了下文章的连接仍是很不错的。html
原文做者是由几个问题展开了说明。java
问题 1: 浏览器的console里会打印出什么?git
var a = 10;
function foo() {
console.log(a); // ??
var a = 20;
}
foo();
复制代码
问题2: 若是是有const或let代替var,输出是否同样?es6
var a = 10;
function foo() {
console.log(a); // ??
let a = 20;
}
foo();
复制代码
问题3: "newArray"中的元素是什么?github
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(tab页)是否仍然响应?浏览器
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关键字声明的变量被提高并在内存中为其赋值为undefined。可是初始化刚好发生在你在代码中写入它们的地方。另外,var声明的变量是函数做用域的,而let和const是块做用域的。因此,这就是这个过程的样子:
var a = 10; // 全局做用域
function foo() {
// 使用var声明的会被提高到函数做用域内顶部.
// 就像: var a;
console.log(a); // 打印 undefined
// 实际初始化值20只发生在这里
var a = 20; // 本地 scope
}
复制代码
笔:对这个不了解的,能够看下这篇文章了解一番。
答案 2: ReferenceError: a is not defined
解释: let和const容许你声明一个变量被限制在一个块级做用域,或语句或表达式中。不像var,这些变量不会被提高,而且具备所谓的temporal dead zone(TDZ)。尝试在TDZ中访问这些变量将抛出一个ReferenceError,由于它们只能在执行到达声明才可被访问。能够阅读词法做用域和执行上下文栈。
var a = 10; // 全局做用域
function foo() { // 进入新的做用域, TDZ开始
// 没有初始绑定的'a'被建立
console.log(a); // ReferenceError
// TDZ 结束, 'a'只是在这里被初始化了一个值20
let a = 20;
}
复制代码
下表概述了与JavaScript中使用的不一样关键字相关的提高行为和范围(主要摘选:Axel Rauschmayer的博客文章)。
答案 3: [3, 3, 3].
解释: 在for loop的头部声明一个带有var关键字的变量,为该变量建立一个绑定(存储空间)。阅读关于闭包的更多信息。让咱们再看一次for循环。
// 误解做用域:认为存在块级做用域
var array = [];
for (var i = 0; i < 3; i++) {
// 三个箭头函数中的每一个都引用同一个绑定,
// 这就是为何循环结束以后返回一样的数字3
array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // [3, 3, 3]
复制代码
若是你声明一个具备块级做用域的变量,则会为每一个循环迭代建立一个新绑定。
// 使用ES6块级做用域绑定
var array = [];
for (let i = 0; i < 3; i++) {
// 这一次,每一个“i”引用一个特定迭代的绑定,并保留当时的值。
// 所以,每一个arrow函数返回一个不一样的值。
array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // [0, 1, 2]
复制代码
解决这个问题的另外一种方法是使用闭包。
let array = [];
for (var i = 0; i < 3; i++) {
array[i] = (function(x) {
return function() {
return x;
};
})(i);
}
const newArray = array.map(el => el());
console.log(newArray); // [0, 1, 2]
复制代码
为啥let能够,能够参考这篇文章
答案 4: 不会
解释: JavaScript并发模型基于“事件循环”。当我说“浏览器是JS的家(归宿)”时,我真正的意思是浏览器提供运行时环境来执行咱们的JavaScript代码。浏览器的主要组件包括 调用堆栈 ,事件循环 ,任务队列 和 Web API 。像setTimeout,setInterval和Promise这样的全局函数不是JavaScript的一部分,而是Web API的一部分。JavaScript环境的可视化表示以下所示:
JS调用堆栈是后进先出(LIFO)。引擎一次从堆栈中获取一个函数,并从上到下依次运行代码。每次遇到一些异步代码(如setTimeout)时,它都会将其交给Web API(箭头1)。所以,每当触发事件时,callback都会被发送到任务队列(箭头2)。Event Loop不断监视任务队列,并按照排队顺序一次处理一个callback。每当调用堆栈为空时,循环检索回调并将其放入堆栈(箭头3)进行处理。请记住,若是调用堆栈不为空,则事件循环不会将任何callbacks推送到堆栈。
有关Event Loop如何在JavaScript中工做的更详细说明,我强烈建议您观看Philip Roberts的视频。此外,你还能够经过这个很是棒的工具可视化和理解调用堆栈。来吧,在那里运行'foo'函数,看看会发生什么!
如今,有了这些知识,让咱们试着回答上述问题:
笔:其实这个答案里的连接和下面答案的连接很给力了。
不过也能够看看其余的
答案 5: 不会
解释: 大多数时候,我看到开发人员假设在事件循环的蓝图中只有一个任务队列(笔: 也叫task queue或event queue或callback queue )。但事实并不是如此。咱们能够有多个任务队列。由浏览器选择任意的队列并在其中处理callbacks。
在高层次上来看,JavaScript中有宏任务和微任务。setTimeout回调是 macrotasks 而Promise回调是 microtasks 。主要的区别在于他们的执行仪式。宏任务在单个循环周期中一次一个地推入堆栈,可是微任务队列老是在执行返回到event loop(包括任何额外排队的项)以前清空。所以,若是你将这些项快速的添加到这个你正在处理的队列,那么你将永远在处理微任务。有关更深刻的解释,请观看Jake Archibald的视频或文章。
在执行返回事件循环以前,微任务队列老是被清空
如今,当你在控制台中运行如下代码段时:
function foo() {
return Promise.resolve().then(foo);
};
复制代码
每次调用'foo'都会继续在微任务队列上添加另外一个'foo'回调,所以事件循环没法继续处理其余事件(scroll,click等),直到该队列彻底清空为止。所以,它会阻止渲染。
笔:Jake的此文绝对是精华,没有读过的能够拜读一番。
答案 6: 能够, 经过是对象iterables
解释: 拓展运算符和for-of语句迭代iterable对象。数组或Map是具备默认迭代行为的内置iterable。对象不是可迭代的,但你可使用iterable和iterator协议使它们可迭代。
在Mozilla文档中,若是一个对象实现了@@iterator方法,那么它就是可迭代的,这意味着这个对象(或者它原型链上的一个对象)必须有一个带有@@iterator键的属性,这个键能够经过常量Symbol.iterator得到。
上述陈述可能看起来有点冗长,但下面的例子会更有意义:
var obj = { x: 1, y: 2, z: 3 };
obj[Symbol.iterator] = function() {
return {
next: function() {
if (this._countDown === 3) {
const lastValue = this._countDown;
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]
复制代码
你还可使用generator函数来自定义对象的迭代行为:
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
Object.setPrototypeOf(obj, { c: 3 });
Object.defineProperty(obj, "d", { value: 4, enumerable: false });
for (let prop in obj) {
console.log(prop);
}
复制代码
笔:对这个不了解的能够看文章了解一下
答案 8: 10
解释: 当咱们将x初始化为全局做用域时,它将成为window对象的属性(假设它是浏览器环境而不是严格模式)。看下面代码:
var x = 10; // 全局做用域
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
复制代码
笔:这个说的主要就是this了,不了解的能够看下
就是这样!若是你全部的答案都正确,那就作得好。错了不可怕,由于咱们都从错误中学习。关键是要知道背后的“缘由”。
你都对了吗老兄。
原文:
Kyle simpson: You don't know js :smile: