【前端面试】之JavaScript基础篇

240 阅读20分钟

一些面试常见的JavaScript基础知识,内容简单,容易理解,熟记就行,被问到的话要尽量回答准确和全面。

1.JavaScript中数组操作常用方法

  • 数组检测: Array.isArray(item)
  • 转换为字符串: arrs.join(',')
  • 栈方法: 栈是一种LIFO(Last-In-First-Out,后进先出)的数据结构
// push()方法可以接收任意数量的参数,把它们逐个添加到数组末尾,并返回修改后数组的长度
// pop()方法则从数组末尾移除最后一项,减少数组的length值,然后返回移除的项
var colors=new Array();//创建一个数组
var count=colors.push("red","green");//推入两项
alert(count);//2
alert(colors);//["red","green"]

count=colors.push("black");//推入另一项
alert(count);//3

var item=colors.pop();//移除最后一项,并返回最后一项的值
alert(item);//“black”
alert(colors.length);//2

// 以上代码首先我们使用push()将两个字符串推入数组的末尾,并将返回的结果保存在变量count中。
// 然后再推入一个值,而结果任然保存在count中.因为此时数组中包含3项,所以push()返回3。
// 在调用pop()时,它会返回数组的最后一项,即字符串"black".此后,数组中仅剩两项。
  • 队列方法: 队列数据结构的访问规则是FIFO(First-In-First-Out,先进先出).
// shift()移除数组中第一个项并返回改项,同时将数组长度减1
// unshift()在数组前端添加任意个项并返回新数组的长度
var n=new Array("张三","李四");
n.push("王五");// 添加参数到数组末尾,并修改数组长度
n.shift();// 移除数组第一项,并返回该项
            
n.unshift("小钱");//添加参数到数组前端
n.pop();// 从数组末尾移除最后一项
            
//循环显示出数组的值
for(var i=0;i<n.length;i++) {
  console.log(n[i]);// 小钱 李四 
}      
  • 排序方法
var values = [0,1,5,10,15];
values.sort();//进行排序显示
alert(values);//0,1,10,15,5

function compare(value1, value2) {
  return value1 - value2;
}
var values = [0,1,5,10,15];
values.sort(compare);
console.log(values); //0,1,5,10,15
  • 操作方法
// concat()方法可以基于当前数组中的所有项创建一个新数组
var colors=["red","green","blue"];
var color2=colors.concat("yellow",["black","brown"]);
alert(color2);//red,green,blue,yellow,black,brown

// slice()方法基于当前数组中的一或多个项创建一个新数组
var colors=["red","green","blue","yellow","black"];
colors.slice(1);//green,blue,yellow,black
colors.slice(1,4)//green,blue,yellow

// splice()方法恐怕要算是最强大的数组方法了,它有很多种用法(删除、插入和替换)
var colors=["red","green","blue"];
var removed=colors.splice(0,1);//删除第一项
alert(colors);//green,blue
alert(removed);//red,返回的数组中只有一项          
removed=colors.splice(1,0,"yellow","orange");//从位置1开始插入2条数据
alert(colors);//green,yellow,orange,blue
alert(removed);//返回一个空数组          
removed=colors.splice(1,1,"red","purple");//从位置1插入2条数据,并删除位置1的数据
alert(colors);//green,red,purple,orange,blue
alert(removed);//yellow,返回的数组中只包含一项
  • 位置方法
// indexOf():从数组的开头(位置0)开始向后查找
// lastIndexOf():从数组的末尾开始向前查找

var numbers=[1,2,3,4,5,4,3,2,1];
alert(numbers.indexOf(4));//3 返回第一个出现的4位置
alert(numbers.lastIndexOf(4));//5 返回最后一个出现的4位置
alert(numbers.indexOf(4,4));//5  从位置4开始查找第一个4出现的位置
alert(numbers.lastIndexOf(4,4));//3 从位置4开始查找最后一个4出现的位置
var person={name:"ToNi"};
var people=[{name:"ToNi"}];
var morePeople=[person];
alert(people.indexOf(person));//返回-1 必须严格相等,不仅仅是值相等
alert(morePeople.indexOf(person));//返回0

2.JavaScript如何设置获取盒模型对应的宽和高

  • dom.style.width/height(只能获取到内联样式的宽和高,输出值带单位)
  • dom.currentStyle.width/height(只有IE浏览器支持,输出值带单位)
  • window.getComputedStyle(dom).width/height(兼容性好,输出值带单位)
  • dom.getBoundingClientRect().width/height(兼容性好,输出值不带单位)

3.DOM事件的级别

  • (1)DOM0: element.onclick = function(){}
  • (2)DOM2: element.addEventListener('click',function(){},false)
  • (3)DOM3: element.addEventListener('keyup',function(){},false)
  • (4)建议结合红宝书第6页

【拓展】
所谓的0级dom与2级dom事件就是不同版本间的差异,具体的说就是,对于不同的dom级别,如何定义事件处理,以及使用时有什么不同。 比如在dom0级事件处理中,后定义的事件会覆盖前面的,但是dom2级事件处理中,对一个按钮点击的时间处理就没有被覆盖掉。

4.DOM事件模型

(1)冒泡型事件处理模型(Bubbling): 冒泡型事件处理模型在事件发生时,首先在最精确的元素上触发,然后向上传播,直到根节点,反映到DOM树上就是事件从叶子节点传播到根节点。

(2)捕获型事件处理模型(Captrue): 相反地,捕获型在事件发生时首先在最顶级的元素上触发,传播到最低级的元素上,在DOM树上的表现就是由根节点传播到叶子节点。

【捕获事件的具体流程】
window==>document==>html==>body==>父级元素==>目标元素

(3)标准的事件处理模型分为三个阶段:

  • 父元素中所有的捕捉型事件(如果有)自上而下地执行
  • 目标元素的冒泡型事件(如果有)
  • 父元素中所有的冒泡型事件(如果有)自下而上地执行

5.Event对象的常见应用

  • (1)event.preventDefault(): 阻止事件的默认行为
  • (2)event.stopPropagation(): 阻止事件的进一步传播,也就是阻止冒泡
  • (3)event.stopImmediatePropagation(): 阻止剩余的事件处理函数的执行,并防止当前事件在DOM树上冒泡。
  • (4)event.currentTarget: 返回绑定事件的元素
  • (5)event.target: 返回触发事件的元素

6.JavaScript类的声明

// 1、类的声明
function Animal() {
  this.name = "name";
}

// 2、ES6中类的声明
class Animal2 {
  constructor() {
    this.name = name;
  }
}

// 3、实例化类
console.log(new Animal(), new Animal2());

7.创建对象有几种方法

(1)字面量:

var k1 = {
  name: 'k1'
};
var k2 = new Object({
  name: 'k2'
});

(2)通过构造函数:

var m = function (name) {
  this.name = name;
};
var k3 = new m('k3');

(3)通过Object.create:

var p = {
  name: 'ppp'
};
var k4 = Object.create(p);

8.JavaScript异步加载的方式

  • (1)动态脚本加载;
  • (2)defer
  • (3)async

9.JavaScript的typeof返回哪些数据类型?

  • object
  • number
  • function
  • boolean
  • underfind
  • string
console.log(typeof undefined); // undefined
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof "str"); // string
console.log(typeof Symbol("foo")); // symbol
console.log(typeof 2172141653n); // bigint
console.log(typeof function () {}); // function
// 不能判别
console.log(typeof []); // object
console.log(typeof {}); // object
console.log(typeof null); // object

【拓展】

  • 比较混淆的是:介绍JavaScript的基本数据类型
  • 答案为:Undefined、Null、Boolean、Number、String、BigInt(可以表示任意大小的整数)、Symbol(创建后独一无二且不可变的数据类型)

10.window.onload和DOMContentLoaded的区别是

window.addEventListener('load',function(){
  //页面的全部资源加载完才会执行,包括图片、视频等
})
document.addEventListener('DOMContentLoaded',function(){
  //DOM渲染完即可执行,此时图片、视频还可能没有加载完
})

11.JavaScript中有哪些内置函数,与内置对象的区别是什么

  • (1)内置函数: 是浏览器内核自带的,不用任何函数库引入就可以直接使用的函数,如常规函数(alert等)、数组函数(reverse等)、日期函数(getDate等)、数学函数(floor等)、字符串函数(length等);
  • (2)内置对象: 是浏览器本身自带的,内置对象中往往包含了内置函数。内置对象有Object、Array、Boolean、Number、String、Function、Date、RegExp等。

12.JavaScript变量按照存储方式区分为哪些类型,并描述其特点

//值类型
//变量的交换,按值访问,操作的是他们实际保存的值。
//等于在一个新的地方按照新的标准开了一个空间(栈内存),
//这样a的值对b的值没有任何影响
var a = 100;
var b = a;
b = 200;
console.log("a:" + a + ",b:" + b); //a:100,b:200

//引用类型
//变量的交换,当查询时,我们需要先从栈中读取内存地址,
//然后再顺藤摸瓜地找到保存在堆内存中的值;
//发现当复制的是对象,那么obj1和obj2两个对象被串联起来了,
//obj1变量里的属性被改变时候,obj2的属性也被修改。
var obj1 = {
  x: 100
};
var obj2 = obj1;
obj2.x = 200;
console.log("obj1:" + obj1.x + ",obj2:" + obj2.x); //obj1:200,obj2:200

13.如何理解JSON

  • (1)JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。它是基于JavaScript的一个子集。数据格式简单, 易于读写, 占用带宽小,比如:{"age":"12", "name":"back"}
  • (2)JSON字符串转换为JSON对象:
var obj =eval('('+ str +')');
var obj = str.parseJSON();
var obj = JSON.parse(str);
  • (3)JSON对象转换为JSON字符串:
var last=obj.toJSONString();
var last=JSON.stringify(obj);

14.JavaScript中&&和||

  • (1)只要“||”前面为false,不管“||”后面是true还是false,都返回“||”后面的值;
  • (2)只要“||”前面为true,不管“||”后面是true还是false,都返回“||”前面的值。
  • (3)且在js逻辑运算中,0、”“、null、false、undefined、NaN都会判为false,其他都为true。
  • (4)只要“&&”前面是false,无论“&&”后面是true还是false,结果都将返“&&”前面的值;
  • (5)只要“&&”前面是true,无论“&&”后面是true还是false,结果都将返“&&”后面的值。

15.什么是构造函数

(1)构造函数就是初始化一个实例对象,对象的prototype属性是继承一个实例对象。

(2)注意事项:

  • 默认函数首字母大写;
  • 构造函数并没有显示返回任何东西。new 操作符会自动创建给定的类型并返回他们,当调用构造函数时,new会自动创建this对象,且类型就是构造函数类型;
  • 也可以在构造函数中显示调用return.如果返回的值是一个对象,它会代替新创建的对象实例返回。如果返回的值是一个原始类型,它会被忽略,新创建的实例会被返回;
  • 因为构造函数也是函数,所以可以直接被调用,但是它的返回值为undefine,此时构造函数里面的this对象等于全局this对象。this.name其实就是创建一个全局的变量name。在严格模式下,当你补通过new 调用Person构造函数会出现错误;

16.函数声明和函数表达式的区别

//成功
fn()
function fn() {
  console.log("函数声明,全局,有变量提升的能力");
}

//报错
fn1()
var fun1 = function () {
  console.log("函数表达式,局部,没有变量提升的能力")
}

17.说一下对变量提升的理解

(1)顾名思义,就是把下面的东西提到上面。在JS中,就是把定义在后面的东东(变量或函数)提升到前面中定义。

var v = 'Hello World';
(function () {
  alert(v); //undefined
  var v = 'I love you';
})()

(2)根据上面变量提升原件以及js的作用域(块级作用域)的分析,得知 上面代码真正变成如下:

var v = 'Hello World';
(function () {
  var v;
  alert(v);
  v = 'I love you';
})()

(3)在我们写js code 的时候,我们有2中写法,一种是函数表达式,另外一种是函数声明方式。我们需要重点注意的是,只有函数声明形式才能被提升。

//成功
function myTest() {
  foo();
  function foo() {
    alert("我来自 foo");
  }
}
myTest();

//失败
function myTest() {
  foo();
  var foo = function foo() {
    alert("我来自 foo");
  }
}
myTest();

18.说一下this几种不同的使用场景

(1)作为构造函数执行,如果函数创建的目的是使用new来调用,并产生一个对象,那么此函数被称为构造器函数;

var Niu = function (string) {
  this.name = string;
};

(2)作为对象属性执行,对象成员方法中的this是对象本身,此时跟其他语言是一致的,但是也有差异,JavaScript中的this到对象的绑定发生在函数调用的时候;

var myObj = {
  value: 0,
  increment: function (inc) {
    this.value += typeof inc === 'number' ? inc : 1;
  }
};
myObj.increment(); //1
myObj.increment(2); //3

(3)作为普通函数执行,以普通方式定义的函数中的this:会被自动绑定到全局对象;

var value = 232;
function toStr() {  
  console.log(this.value);
}

(4)对象方法中闭包函数的this

//在以普通方式定义的函数中的this会被自动绑定到全局对象上,
//大家应该可以看出闭包函数定义也与普通方式无异,因此他也会被绑定到全局对象上。
value = 10;
var closureThis = {
  value: 0,
  acc: function () {
    var helper = function () {
      this.value += 2;
      console.log("this.value : %d", this.value);
    }
    helper();
  }
};
closureThis.acc(); //12
closureThis.acc(); //14

var closureThat = {
  value: 0,
  acc: function () {
    that = this;
    var helper = function () {
      that.value += 2;
      console.log("that.value : %d", that.value);
    }
    helper();
  }
};
closureThat.acc(); // 2
closureThat.acc(); // 4

(5)apply函数的参数this,apply方法允许我们选择一个this值作为第一个参数传递,第二个参数是一个数组,表明可以传递多个参数。

19.什么是自由变量

自由变量就是当前作用域没有定义的变量,即“自由变量”

var a = 100;
function F1() {
  var b = 200;
  function F2() {
    var c = 300;
    //a是自由变量
    console.log(a);
    //b是自由变量
    console.log(b);
    console.log(c);
  }
  F2();
}
F1();

20.同步和异步的区别是什么?分别举一个同步和异步的例

同步会阻塞代码执行,而异步不会;alert是同步,setTimeout是异步。

21.JavaScript中的异步和单线程

  • (1)其实,单线程和异步确实不能同时成为一个语言的特性。js选择了成为单线程的语言,所以它本身不可能是异步的,但js的宿主环境(比如浏览器,Node)是多线程的,宿主环境通过某种方式(事件驱动,下文会讲)使得js具备了异步的属性。
  • (2)js是单线程语言,浏览器只分配给js一个主线程,用来执行任务(函数),但一次只能执行一个任务,这些任务形成一个任务队列排队等候执行,但前端的某些任务是非常耗时的,比如网络请求,定时器和事件监听,如果让他们和别的任务一样,都老老实实的排队等待执行的话,执行效率会非常的低,甚至导致页面的假死。所以,浏览器为这些耗时任务开辟了另外的线程,主要包括http请求线程,浏览器定时触发器,浏览器事件触发线程,这些任务是异步的。

22.什么是跨域

  • (1)浏览器有同源策略,不允许ajax访问其他域接口。跨域条件(协议、域名、端口)有一个不同就算是跨域。
  • (2)可以跨域的三个标签(<img src=xxx>、<link href=xxx>、<script src=xxx>)。三个标签的应用场景(<img>用于打点统计,统计网站可能是其他域、<link><script>可以使用CDN,CDN的也是其他域、<script>可以用于JSOP)。
  • (3)跨域注意的事项:所有的跨域请求都必须经过信息提供方允许、如果未经过允许即可获取,那是浏览器同源策略出现漏洞。

23.解决跨域的几种方案

  • (1)通过jsonp跨域:在js中,我们直接用XMLHttpRequest请求不同域上的数据时,是不可以的。但是,在页面上引入不同域上的js脚本文件却是可以的,jsonp正是利用这个特性来实现的。
  • (2)通过修改document.domain来跨子域
  • (3)使用window.name来进行跨域
  • (4)使用HTML5中新引进的window.postMessage方法来跨域传送数据

24.null,undefined 的区别

  • (1)null:表示一个对象是“没有值”的值,也就是值为“空”;
  • (2)undefined:表示一个变量声明了没有初始化(赋值);

【注意】
在验证null时,一定要使用 === ,因为 == 无法分别 null 和undefined

25.JavaScript的作用域链式什么

全局函数无法查看局部函数的内部细节,但局部函数可以查看其上层的函数细节,直至全局细节。当需要从局部函数查找某一属性或方法时,如果当前作用域没有找到,就会上溯到上层作用域查找,直至全局函数,这种组织形式就是作用域链。

26.Javascript中,有一个函数,执行时对象查找时,永远不会去查找原型,这个函数是

(1)javaScript中hasOwnProperty函数方法是返回一个布尔值,指出一个对象是否具有指定名称的属性。此方法无法检查该对象的原型链中是否具有该属性,该属性必须是对象本身的一个成员。

(2)使用方法:

  • object.hasOwnProperty(proName)
  • 其中参数object是必选项。一个对象的实例。
  • proName是必选项。一个属性名称的字符串值。
  • 如果 object 具有指定名称的属性,那么JavaScript中hasOwnProperty函数方法返回 true,反之则返回 false。

27.异步加载JS的方式有哪些

  • (1)defer,只支持IE;
  • (2)async:
  • (3)创建script,插入到DOM中,加载完毕后callBack

28.ajax是什么?同步和异步的区别?

  • (1)ajax是一种无需重新加载整个网页的情况下,能够更新部分网页的技术。
  • (2)同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去;异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。

29.事件委托是什么,举个例子

它还有一个名字叫事件代理,JavaScript高级程序设计上讲:事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。那这是什么意思呢?网上的各位大牛们讲事件委托基本上都用了同一个例子,就是取快递来解释这个现象。

<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
    <style>
      ul li {
        display: block;
        height: 50px;
        width: 100px;
        text-align: center;
        line-height: 50px;
        background-color: #888;
        margin-bottom: 8px;
      }
    </style>
  </head>
  <body>
    <ul id="Inul">
      <li id="item1">111</li>
      <li id="item2">222</li>
      <li id="item3">333</li>
      <li id="item4">444</li>
      <li id="item5">555</li>
    </ul>
    <script>
      //不使用事件委托
      /*window.onload = function(){
      	var oUL = document.getElementById("Inul");
      	var aLi = oUL.getElementsByTagName("li");
      	for(var i=0;i<aLi.length;i++){
      	  aLi[i].onclick = function(){
      	    alert(123);
      	  }
      	}
      }*/
      //使用事件委托
      window.onload = function () {
        var oUL = document.getElementById("Inul");
        oUL.onclick = function (e) {
          console.log(e)
          console.log(e.target.nodeName)
          console.log(e.target.id)
          alert(123);
        }
      }
    </script>
  </body>
</html>

使用事件委托的意义是为了减少代码的DOM操作,提高程序性能。

30.如何实现JavaScript模块化,请至少使用两种方法

  • (1)立即执行函数(IIFE): 可以达到不暴露私有成员的目的。
  • (2)对象写法: 可以把模块写成一个对象,所有的模块成员都放到这个对象里面。

31.高阶函数是什么,怎么去写一个高阶函数

  • 高阶函数: 参数值为函数或者返回值为函数。例如map,reduce,filter,sort方法就是高阶函数。
  • 编写高阶函数,就是让函数的参数能够接收别的函数。

32.typeof 是否能正确判断类型?

typeof 对于原始类型来说,除了 null 都可以显示正确的类型

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'

typeof 对于对象来说,除了函数都会显示 object,所以说 typeof 并不能准确判断变量到底是什么类型

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'

33.instanceof 能正确判断对象的原理是什么?

如果我们想判断一个对象的正确类型,这时候可以考虑使用 instanceof,因为内部机制是通过原型链来判断的

const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true

var str = 'hello world'
str instanceof String // false

var str1 = new String('hello world')
str1 instanceof String // true

对于原始类型来说,你想直接通过 instanceof 来判断类型是不行的,当然我们还是有办法让 instanceof 判断原始类型的

class PrimitiveString {
  static [Symbol.hasInstance](x) {
    return typeof x === 'string'
  }
}
console.log('hello world' instanceof PrimitiveString) // true

你可能不知道 Symbol.hasInstance 是什么东西,其实就是一个能让我们自定义 instanceof 行为的东西,以上代码等同于 typeof 'hello world' === 'string',所以结果自然是 true 了。这其实也侧面反映了一个问题, instanceof 也不是百分之百可信的。

34.数组转字符串以及字符串转数组的方法

// 字符串转数组
var str = "abcdef";
console.log(str.split("")); // ['a', 'b', 'c', 'd', 'e', 'f']

// 数组转字符串
var arr = [1,3,4,56,2];
console.log(arr.join("")); // 134562

35.箭头函数与普通函数function的区别是什么?

  • 箭头函数语法上比普通函数更加简洁(ES6中每一种函数都可以使用形参赋默认值和剩余运算符)
function fn(x){
  return function(y){
    return x+y
  }
}

let fn = x => y => x+y
  • 箭头函数没有自己的this,它里面出现的this是继承函数所处上下文的this。(使用call/apply等任何方式都无法改变this的指向)
let obj = {
  name: 'hah'
}
function fn1(){
  console.log(this)
}
fn1.call(obj) //{name: 'hah'}

let fn2 = ()=>{
  console.log(this)
}
fn2.call(obj) //window

document.body.onclick = function() {
  console.log(this) // body
}
document.body.onclick = () => {
  console.log(this) // window
}
  • 箭头函数没有arguments(类数组),智能基于...arg获取传递的参数集合(数组)
let fn = (...arg)=>{
  console.log(arguments) //arguments is not defined
  console.log(arg) //[10,20,30]
}
fn(10,20,30)
  • 箭头函数不能使用new生成实例,因为自身没有this和prototype

36.==和=== 一般是在什么情况下使用?

除了 == null 之外,其他都一律使用 === ,例如

const obj = { x: 100 }
if(obj.a == null) {}
// 相当于
if(obj.a === null || obj.a === undefined) {}

37.箭头函数需要注意的地方

当要求动态上下文的时候,就不能够使用箭头函数,也就是this的固定化。

  • 1、在使用=>定义函数的时候,this的指向是定义时所在的对象,而不是使用时所在的对象;
  • 2、不能够用作构造函数,这就是说,不能够使用new命令,否则就会抛出一个错误;
  • 3、不能够使用arguments对象;
  • 4、不能使用yield命令;

下面来看一道面试题,重点说明下第一个知识点:

class Animal {
  constructor() {
    this.type = "animal";
  }
  say(val) {
    setTimeout(function () {
      console.log(this); //window
      console.log(this.type + " says " + val);
    }, 1000)
  }
}
var animal = new Animal();
animal.say("hi"); //undefined says hi

【拓展】 《JavaScript高级程序设计》第二版中,写到:“超时调用的代码都是在全局作用域中执行的,因此函数中this的值在非严格模式下指向window对象,在严格模式下是undefined”。也就是说在非严格模式下,setTimeout中所执行函数中的this,永远指向window!!

我们再来看看箭头函数(=>)的情况:

class Animal {
  constructor() {
    this.type = "animal";
  }
  say(val) {
    setTimeout(() => {
      console.log(this); //Animal
      console.log(this.type + ' says ' + val);
    }, 1000)
  }
}
var animal = new Animal();
animal.say("hi"); //animal says hi

【特点】

  • 不需要function关键字来创建函数
  • 省略return关键字
  • 继承当前上下文的 this 关键字

38.let和const

let是更完美的var,不是全局变量,具有块级函数作用域,大多数情况不会发生变量提升。const定义常量值,不能够重新赋值,如果值是一个对象,可以改变对象里边的属性值。

  • 1、let声明的变量具有块级作用域
  • 2、let声明的变量不能通过window.变量名进行访问
  • 3、形如for(let x..)的循环是每次迭代都为x创建新的绑定

下面是var带来的不合理场景

var arr = [];
for (var i = 0; i < 10; i++) {
  arr[i] = function () {
    console.log(i);
  }
}
arr[5]() //10,a[5]输出f(){console.log(i);},后面加个括号代表执行f()

在上述代码中,变量i是var声明的,在全局范围类都有效,所以用来计数的循环变量泄露为全局变量。所以每一次循环,新的i值都会覆盖旧值,导致最后输出都是10。

而如果对循环使用let语句的情况,那么每次迭代都是为x创建新的绑定代码如下:

var arr = [];
for (let i = 0; i < 10; i++) {
  arr[i] = function () {
    console.log(i);
  }
}
arr[5]() //5,a[5]输出f(){console.log(i);},后面加个括号代表执行f()

【拓展】 当然,除了这种方式让数组找中的各个元素分别是不同的函数,我们还可以采用ES5中的闭包和立即函数两种方法。

1、采用闭包

function showNum(i) {
  return function () {
    console.log(i)
  }
}
var a = []
for (var i = 0; i < 5; i++) {
  a[i] = showNum(i)(); //循环输出1,2,3,4
}

2、采用立即执行函数

var a = []
for (var i = 0; i < 5; i++) {
  a[i] = (function (i) {
    return function () {
      console.log(i)
    }
  })(i)
}
a[2](); //2

【面试】 把以下代码使用两种方法,依次输出0-9

var funcs = []
for (var i = 0; i < 10; i++) {
  funcs.push(function () {
    console.log(i)
  })
}
funcs.forEach(function (func) {
  func(); //输出十个10
})

方法一:使用立即执行函数

var funcs = []
for (var i = 0; i < 10; i++) {
  funcs.push((function (value) {
    return function () {
      console.log(value)
    }
  }(i)))
}
funcs.forEach(function (func) {
  func(); //依次输出0-9
})

方法二:使用闭包

function show(i) {
  return function () {
    console.log(i)
  }
}
var funcs = []
for (var i = 0; i < 10; i++) {
  funcs.push(show(i))
}
funcs.forEach(function (func) {
  func(); //0 1 2 3 4 5 6 7 8 9
})

方法三:使用let

var funcs = []
for (let i = 0; i < 10; i++) {
  funcs.push(function () {
    console.log(i)
  })
}
funcs.forEach(function (func) {
  func(); //依次输出0-9
})

其他知识点:forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数。戳这里查看参考文章

39.Set数据结构

es6方法,Set本身是一个构造函数,它类似于数组,但是成员值都是唯一的。

const set = new Set([1,2,3,4,4])
console.log([...set] )// [1,2,3,4]
console.log(Array.from(new Set([2,3,3,5,6]))); //[2,3,5,6]

40.Class的讲解

class语法相对原型、构造函数、继承更接近传统语法,它的写法能够让对象原型的写法更加清晰、面向对象编程的语法更加通俗,下面是class的具体用法。

class Animal {
  constructor() {
    this.type = 'animal'
  }
  says(say) {
    console.log(this.type + 'says' + say)
  }
}
let animal = new Animal()
animal.says('hello') // animal says hello

class Cat extends Animal {
  constructor() {
    super()
    this.type = 'cat'
  }
}
let cat = new Cat()
cat.says('hello') // cat says hell

可以看出在使用extend的时候结构输出是cat says hello 而不是animal says hello。说明contructor内部定义的方法和属性是实例对象自己的,不能通过extends 进行继承。在class cat中出现了super(),这是什么呢?因为在ES6中,子类的构造函数必须含有super函数,super表示的是调用父类的构造函数,虽然是父类的构造函数,但是this指向的却是cat。更详细的参考文章

41.模板字符串

就是这种形式${varible},在以往的时候我们在连接字符串和变量的时候需要使用这种方式'string' + varible + 'string'但是有了模版语言后我们可以使用string${varible}string这种进行连接。基本用途有如下:

1、基本的字符串格式化,将表达式嵌入字符串中进行拼接,用${}来界定。

//es5 
var name = 'lux';
console.log('hello' + name);
//es6
const name = 'lux';
console.log(`hello ${name}`); //hello lux

2、在ES5时我们通过反斜杠()来做多行字符串或者字符串一行行拼接,ES6反引号(``)直接搞定。

//ES5
var template = "hello \
world";
console.log(template); //hello world

//ES6
const template = `hello
world`;
console.log(template); //hello 空行 world

【拓展】 字符串的其他方法

// 1.includes:判断是否包含然后直接返回布尔值
let str = 'hahay'
console.log(str.includes('y')) // true

// 2.repeat: 获取字符串重复n次
let s = 'he'
console.log(s.repeat(3)) // 'hehehe'

42.什么是变量提升?什么是暂时性死区?var、let 及 const 区别?

对于这个问题,我们应该先来了解提升(hoisting)这个概念。

console.log(a) // undefined
var a = 1

从上述代码中我们可以发现,虽然变量还没有被声明,但是我们却可以使用这个未被声明的变量,这种情况就叫做提升,并且提升的是声明。对于这种情况,我们可以把代码这样来看

var a
console.log(a) // undefined
a = 1

接下来我们再来看一个例子

var a = 10
var a
console.log(a)

对于这个例子,如果你认为打印的值为 undefined 那么就错了,答案应该是 10,对于这种情况,我们这样来看代码

var a
var a
a = 10
console.log(a)

到这里为止,我们已经了解了 var 声明的变量会发生提升的情况,其实不仅变量会提升函数也会被提升。

console.log(a) // ƒ a() {}
function a() {}
var a = 1

对于上述代码,打印结果会是 ƒ a() {},即使变量声明在函数之后,这也说明了函数会被提升,并且优先于变量提升。

说完了这些,想必也知道 var 存在的问题了,使用 var 声明的变量会被提升到作用域的顶部,接下来我们再来看 let 和 const 。我们先来看一个例子:

var a = 1
let b = 1
const c = 1
console.log(window.b) // undefined
console.log(window.c) // undefined

function test(){
  console.log(d) // Uncaught ReferenceError: Cannot access 'd' before initialization
  let d
}
test()

首先在全局作用域下使用 let 和 const 声明变量,变量并不会被挂载到 window 上,这一点就和 var 声明有了区别。再者当我们在声明 d 之前如果使用了 d,就会出现报错的情况。

你可能会认为这里也出现了提升的情况,但是因为某些原因导致不能访问。

首先报错的原因是因为存在暂时性死区,我们不能在声明前就使用变量,这也是 let 和 const 优于 var 的一点。然后这里你认为的提升和 var 的提升是有区别的,虽然变量在编译的环节中被告知在这块作用域中可以访问,但是访问是受限制的。

那么到这里,想必大家也都明白 var、let 及 const 区别了,不知道你是否会有这么一个疑问,为什么要存在提升这个事情呢,其实提升存在的根本原因就是为了解决函数间互相调用的情况

function test1() {
  test2()
}
function test2() {
  test1()
}
test1()

假如不存在提升这个情况,那么就实现不了上述的代码,因为不可能存在 test1 在 test2 前面然后 test2 又在 test1 前面。下面来个总结

  • 函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部
  • var 存在提升,我们能在声明之前使用。let、const 因为暂时性死区的原因,不能在声明前使用
  • var 在全局作用域下声明变量会导致变量挂载在 window 上,其他两者不会
  • let 和 const 作用基本一致,但是后者声明的变量不能再次赋值

43.数据类型的判断有哪几种方法

  • typeof:能判断所有值类型,函数。不可对 null、对象、数组进行精确判断,因为都返回 object 。
console.log(typeof undefined); // undefined
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof "str"); // string
console.log(typeof Symbol("foo")); // symbol
console.log(typeof 2172141653n); // bigint
console.log(typeof function () {}); // function

// 不能判别
console.log(typeof []); // object
console.log(typeof {}); // object
console.log(typeof null); // object
  • instanceof:能判断对象类型,不能判断基本数据类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。比如考虑以下代码:
class People {}
class Student extends People {}

const vortesnail = new Student();

console.log(vortesnail instanceof People); // true
console.log(vortesnail instanceof Student); // true

// 其实现就是顺着原型链去找,如果能找到对应的 `Xxxxx.prototype`  即为 `true` 。比如这里的 
// `vortesnail`  作为实例,顺着原型链能找到 `Student.prototype`  及 `People.prototype` ,所以都为 `true` 。
  • Object.prototype.toString.call() :所有原始数据类型都是能判断的,还有 Error 对象,Date 对象等。
Object.prototype.toString.call(2); // "[object Number]"
Object.prototype.toString.call(""); // "[object String]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(Math); // "[object Math]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(function () {}); // "[object Function]"

44.如何判断变量是否为数组?

Array.isArray(arr); // true
arr.__proto__ === Array.prototype; // true
arr instanceof Array; // true
Object.prototype.toString.call(arr); // "[object Array]"

未完...待续,持续更新