JavaScript是前端开发中很是重要的一门语言,浏览器是他主要运行的地方。JavaScript是一个很是有意思的语言,可是他有不少一些概念,你们常常都会忽略。好比说,原型,闭包,原型链,事件循环等等这些概念,不少JS开发人员都研究很少。前端
因此今天,就来和你们看看下面几个问题,你们能够先思考一下,尝试做答。面试
问题1:下面这段代码,浏览器控制台上会打印什么?数组
问题2:若是咱们使用 let 或 const 代替 var,输出是否相同浏览器
问题3:“newArray”中有哪些元素?闭包
问题4:若是咱们在浏览器控制台中运行'foo'函数,是否会致使堆栈溢出错误?并发
问题5: 若是在控制台中运行如下函数,页面(选项卡) 是否会有响应异步
问题6: 咱们可否以某种方式为下面的语句使用展开运算而不致使类型错误函数
问题7:运行如下代码片断时,控制台上会打印什么?
问题8:xGetter() 会打印什么值?oop
前面的问题咱们都举例出来了,接下来咱们会从头至尾,一个个来分析咱们这些问题的答案,给你们一些学习的思路学习
问题1:
使用var关键字声明的变量在JavaScript中会被提高,并在内存中开辟空间,因为没有赋值,没法定义数值类型,因此分配默认值undefined。var声明的变量,真正的数值初始化,是发生在你肯定赋值的位置。同时,咱们要知道,var声明的变量是函数做用域的,也就是咱们须要区分局部变量和全局变量,而let和const是块做用域的。因此咱们这道题的运行过程是这样的:
var a = 10; // 全局做用域,全局变量。a=10 function foo() { // var a //的声明将被提高到到函数的顶部。 // 好比:var a console.log(a); // 打印 undefined // 实际初始化值20只发生在这里 var a = 20; // local scope }
图解在下面,好理解一点
因此问题1的答案是:undefined
问题 2:
let和const声明可让变量在其做用域上受限于它所在的块、语句或表达式中。和var不一样的地方在于,这两个声明的变量,不会被提高。而且咱们会有一个称为暂时死区(TDZ)。若是访问TDZ中的变量的话,就会报ReferenceError,由于他们的的做用域是在他们声明的位置的,不会有提高。因此必须在执行到声明的位置才能访问。
var a = 10; // 全局使用域 function foo() { // TDZ 开始 // 建立了未初始化的'a' console.log(a); // ReferenceError // TDZ结束,'a'仅在此处初始化,值为20 let a = 20; }
图解:
问题2答案:ReferenceError: a is not defined
问题3:
这个问题,是循环结构会给你们带来一种块级做用域的误区,在for的循环的头部使用var声明的变量,就是单个声明的变量绑定(单个存储空间)。在循环过程当中,这个var声明的i变量是会随循环变化的。可是在循环中执行的数组push方法,最后其实是push了i最终循环结束的3这个值。因此最后push进去的全都是3。
// 误解做用域:认为存在块级做用域 var array = []; for (var i = 0; i < 3; i++) { // 三个箭头函数体中的每一个'i'都指向相同的绑定, // 这就是为何它们在循环结束时返回相同的值'3'。 array.push(() => i); } var newArray = array.map(el => el()); console.log(newArray); // [3, 3, 3]
图解:
若是想记录每一次循环的值下来,可使用let声明一个具备块级做用域的变量,这样为每一个循环迭代建立一个新的绑定。
// 使用ES6块级做用域 var array = []; for (let i = 0; i < 3; i++) { // 这一次,每一个'i'指的是一个新的的绑定,并保留当前的值。 // 所以,每一个箭头函数返回一个不一样的值。 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]
问题3答案:3,3,3
问题4
JavaScript的并发模式基于咱们常说的”事件循环“。
浏览器是提供运行时环境来给咱们执行JS代码的。浏览器的主要组成包括有调用堆栈,事件循环,任务队列和WEB API。像什么经常使用的定时器setTimeout,setInterval这些全局函数就不是JavaScript的一部分,而是WEB API给咱们提供的。
JS调用栈是后进先出(LIFO)的。引擎每次从堆栈中取出一个函数,而后从上到下依次运行代码。每当它遇到一些异步代码,如setTimeout,它就把它交给Web API(箭头1)。所以,每当事件被触发时,callback 都会被发送到任务队列(箭头2)。
事件循环(Event loop)不断地监视任务队列(Task Queue),并按它们排队的顺序一次处理一个回调。每当调用堆栈(call stack)为空时,Event loop获取回调并将其放入堆栈(stack )(箭头3)中进行处理。请记住,若是调用堆栈不是空的,则事件循环不会将任何回调推入堆栈。
好了,如今有了前面这些知识,咱们能够看一下这道题的讲解过程:
实现步骤:
问题4答案:堆栈不会溢出。
问题5:
在不少时候,不少作前端开发的同窗都是认为循环事件图中就只会有一个任务列表。但事实上不是这样的,咱们是能够有多个任务列表的。由浏览器选择其中一个队列并在该队列进行处理回调。
从底层来看,JavaScript中是能够有宏认为和微任务的,好比说setTimeout回调是宏任务,而Promise回调是微任务。
他们有什么区别呢?
主要的区别在于他们的执行方式。宏任务在单个循环周期中一次一个低堆入堆栈,可是微任务队列老是在执行后返回到事件以前清空。因此,若是你以处理条目的速度向这个队列添加条目,那么你就永远在处理微任务。只有当微任务队列为空时,事件循环才会从新渲染页面。
而后咱们再回到咱们前面讲的问题5中:
function foo() { return Promise.resolve().then(foo); };
咱们这段代码,每次咱们去调用【foo】的时候,都会在微任务队列上加另外一个【foo】的回调,所以事件循环没办法继续去处理其余的事件了(好比说滚动,点击事件等等),直到该队列彻底清空位置。所以,不会执行渲染,会被阻止。
问题5答案:不会响应。
问题6:
在咱们作面试题的时候,展开语法和for-of语句去遍历iterable对象定义要遍历的数据。其中咱们要使用迭代器的时候,Array和Map都是有默认迭代操做的内置迭代器的。
可是,对象是不可迭代的,也就是咱们这道题里的,这是一个对象的集合。可是咱们可使用iterable和iterator协议来把它变成能够迭代的。
在咱们研究对象的时候,若是一个对象他实现了@@iterator方法,那么它就是能够迭代的。这意味着这个对象(在他的原型链上的一个对象)必须是又@@iterator键的属性的,而后咱们就能够利用这个键,经过常量Symbol.iterator得到。
下面是这道题的举例写法:
var obj = { x: 1, y: 2, z: 3 }; obj[Symbol.iterator] = function() { // iterator 是一个具备 next 方法的对象, // 它的返回至少有一个对象 // 两个属性:value&done。 // 返回一个 iterator 对象 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]; // 打印 [1, 2, 3]
问题6答案:如上是一种方案,能够避免TypeError异常。
问题7:
在看这个问题的时候,咱们要先理解for-in循环遍历自己的可枚举属性和对象从原来的原型继承来的属性。可枚举属性是能够在for-in循环期间能够访问的属性。
当咱们知道这个知识点前提了以后,咱们在看这道题,你就知道这道题打印的其实就是只能打印这些特定的属性。
var obj = { a: 1, b: 2 }; //a,b 都是可枚举属性 // 将{c:3}设置为'obj'的原型, // 而且咱们知道for-in 循环也迭代 obj 继承的属性 // 从它的原型,'c'也能够被访问。 Object.setPrototypeOf(obj, { c: 3 }); // 咱们在'obj'中定义了另一个属性'd', // 可是将'enumerable'可枚举设置为false。 这意味着'd'将被忽略。 Object.defineProperty(obj, "d", { value: 4, enumerable: false }); //因此最后使用for-in遍历这个对象集合,那就是只能遍历出可枚举属性 for (let prop in obj) { console.log(prop); } // 也就是只能打印 // a // b // c
图解
问题7答案:a、b、c
问题8:
首先咱们能够看到var x是一个全局遍历,在不是严格模式下,这个X就直接是window对象的属性了。在这段代码里,咱们最重要是要理解this的对象指向问题,this始终是指向调用方法的对象的。因此,在foo,xGetter()的状况下,this指向的是foo对象,返回的就是在foo中的属性x,值就是90。可是在xGetter()的状况下,他是直接调用的foo的getx()方法,可是其中this的指向是在xGetter的做用域,就是指向的window对象中,这时指向的就是全局变量x了,值也就是10。
var x = 10; // 全局变量 var foo = { x: 90,//foo对象的内部属性 getX: function() { return this.x; } }; foo.getX(); // 此时是指向的foo对象, //因此打印的是X属性 值就是90 let xGetter = foo.getX;//xGetter是在全局做用域, //这里的this就是指向window对象 xGetter(); // 打印 10
问题8答案:10
ok,咱们的8道问题都解决了,若是你前面写的答案所有都正确,那么你很是棒!去面试前端工做起码12k起步了。就算作不出来或者作错了也没有关系,咱们都是不断经过犯错来学习的,一步步的理解错误,理解背后的缘由,才能进步。
更多技术好文,前端开发学习教程,欢迎关注公众号【前端研究所】看更多前端技术文章!