JavaScript基础知识八问

JavaScript是前端开发中非常重要的一门语言,浏览器是他主要运行的地方。JavaScript是一个非常有意思的语言,但是他有很多一些概念,大家经常都会忽略。比如说,原型,闭包,原型链,事件循环等等这些概念,很多JS开发人员都研究不多。

所以今天,就来和大家看看下面几个问题,大家可以先思考一下,尝试作答。

问题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); // 是否会导致堆栈溢出错误?
}

问题5: 如果在控制台中运行以下函数,页面(选项卡) 是否会有响应

function foo() {
  return Promise().resolve().then(foo)
}

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

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});
 // 在运行for-in循环时,会打印出来什么?
 for(let prop in obj) {
     console.lo(prop);
 }

问题8:xGetter() 会打印什么值?

var x = 10;
 var foo = {
     x: 90,
     getX: function() {
         return this.x;
     }
 };
 foo.getX(); // 90
 var xGetter = foo.getX;
 xGetter(); // 打印出什么?

答案

问题1:

使用var关键字声明的变量在JavaScript中会被提升,并在内存中开辟空间,由于没有赋值,无法定义数值类型,所以分配默认值undefined。var声明的变量,真正的数值初始化,是发生在你确定赋值的位置。同时,我们要知道,var声明的变量是函数作用域的,也就是我们需要区分局部变量和全局变量,而let和const是块作用域的。所以我们这道题的运行过程是这样的:

var a = 10; // 全局作用域,全局变量, a=10
 function foo() {
     console.log(a); // foo函数内部作用域,此时作用域内a还没有赋值,所以打印undefined
     var a = 20;  // 这里应把声明和赋值拆出来看,var a与a = 20; 其中var a的声明将被提升到函数的顶部,
 }
 foo();

所以问题1的答案是:undefined

问题 2:

let和const声明可以让变量在其作用域上受限于它所在的块、语句或表达式中。和var不同的地方在于,这两个声明的变量,不会被提升。并且我们会有一个称为暂时死区(TDZ)。如果访问TDZ中的变量的话,就会报ReferenceError,因为他们的的作用域是在他们声明的位置的,不会有提升。所以必须在执行到声明的位置才能访问。

var a = 10; // 全局作用域,全局变量, 但是在TDZ中无法访问到
 function foo() {
     // TDZ开始
     console.log(a); // 创建了未初始化的“a”
     // TDZ结束
     let a = 20;  // 由于出现let声明,所以会出现TDZ
 }
 foo();

问题2答案:ReferenceError: a is not defined

问题3:

这个问题,是循环结构会给大家带来一种块级作用域的误区,在for的循环的头部使用var声明的变量,就是单个声明的变量绑定(单个存储空间)。在循环过程中,这个var声明的i变量是会随循环变化的。但是在循环中执行的数组push方法,最后实际上是push了i最终循环结束的3这个值。所以最后push进去的全都是3。

var array = [];
 // for小括号与push()及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声明一个具有块级作用域的变量,这样为每个循环迭代创建一个新的绑定。

var array = [];
 // 由于出现let,存在块级作用域
 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给我们提供的。

JavaScript基础知识八问

JS调用栈是后进先出(LIFO)的。引擎每次从堆栈中取出一个函数,然后从上到下依次运行代码。每当它遇到一些异步代码,如setTimeout,它就把它交给Web API(箭头1)。因此,每当事件被触发时,callback都会被发送到任务队列(箭头2)。

事件循环(Event loop)不断地监视任务队列(Task Queue),并按它们排队的顺序一次处理一个回调。每当调用堆栈(call stack)为空时,Event loop获取回调并将其放入堆栈(stack )(箭头3)中进行处理。请记住,如果调用堆栈不是空的,则事件循环不会将任何回调推入堆栈

好了,现在有了前面这些知识,我们可以看一下这道题的实现步骤:

  1. 调用 foo()会将foo函数放入调用堆栈(call stack)
  2. 在处理内部代码时,JS引擎遇到setTimeout。
  3. 然后将foo回调函数传递给WebAPIs(箭头1)并从函数返回,调用堆栈再次为空
  4. 计时器被设置为0,因此foo将被发送到任务队列(箭头2)。
  5. 由于调用堆栈是空的,事件循环将选择foo回调并将其推入调用堆栈进行处理。
  6. 进程再次重复,堆栈不会溢出。

问题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});
 // 虽然此时obj有4个属性,但是可以被遍历的可枚举类型只有3个
 for(let prop in obj) {
     console.lo(prop);
 }

问题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(); // 90,此时是指向的foo对象
 var xGetter = foo.getX; // xGetter是在全局作用域
 xGetter(); // 这里的this就是指向window对象,打印 10

Like (0)
Donate 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
ZEROZERO
Previous 2020年12月24日
Next 2021年1月13日

相关推荐

  • 深入理解JS原型和继承

    在学习JS中的原型,原型链,继承这些知识之前,必须先了解并掌握基础知识:函数和对象的关系。 我们一直都知道,函数也是对象的一种,因为通过instanceof就可以判断出来。但是函数…

    2019年6月29日
    2.0K
  • 如何判断一个对象为数组

    使用 instanceof 操作符 原理 instanceof 的内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。 使用 instanceof 判断一个对象…

    2020年7月3日
    1.0K
  • 回调函数散记

    今天被将要入职的公司的开发人员询问了一个项目中遇到的问题,关于函数内访问外部函数的情况。大致现象如下:js文件中有两个同级函数FnA和FnB,想在函数FnA中调用FnB。 一看就是…

    2019年8月16日
    1.4K
  • JavaScript 事件委托详解

    基本概念 事件委托,通俗地来讲,就是把一个元素响应事件(click、keydown……)的函数委托到另一个元素; 一般来讲,会把一个或者一组元素的事件委托到…

    2021年3月8日
    990
  • js数组去重(区分object、“NaN”、NaN)

    数组去重在前端面试中比较常见,今天来复习复习。 对这样一个数组进行去重,我尝试了几种方法,大多数不能对对象去重,或者不能区分true和”true”、NaN和…

    2021年2月23日
    1.2K
  • JavaScript 的 this 原理

    有时候会使用一种东西,并不代表你了解它。就像你会写JavaScript代码,能看懂JavaScript代码,但不代表你懂它。 学懂 JavaScript 语言,一个标志就是理解下面…

    2019年8月1日
    1.7K
  • JavaScript中call、apply及bind的深度解析

    函数原型链中的 apply,call 和 bind 方法是 JavaScript 中相当重要的概念,与 this 关键字密切相关,相当一部分人对它们的理解还是比较浅显,所谓js基础…

    2019年8月5日
    1.5K
  • 深入理解JS内存机制

    JS的内存机制在很多前端开发者看来并不是那么重要,但是如果你想深入学习JS,并将它利用好,打造高质量高性能的前端应用,就必须要了解JS的内存机制。对于内存机制理解了以后,一些基本的…

    2019年7月14日
    1.6K
  • 函数防抖与函数节流

    函数防抖 定义 触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间;更直白一点就是:一个需要频繁触发的函数,在规定时间内,只让最后一次生效,…

    2020年7月17日
    1.3K
  • 日常开发 26 个常见的 JavaScript 代码优化方案

    1、NULL、Undefined、”【空】检查 我们在创建新变量赋予一个存在的变量值的时候,并不希望赋予null或undefined,我们可以采用以下简洁的赋值方式。 …

    2021年2月22日
    1.2K