前端面试知识点【JS篇】

JS

写在前面:
首先,本文主要是自己在学习过程中,总结的一些面试中会遇到的常见知识点。主要是方便自己随时查看复习,当然能为友友们提供帮助也是我的荣幸。
然后,内容没有分类,基本上是遇到了就会补充,会有点乱,希望文章的目录可以帮到您。
另外,如果不出意外的话,会有一些错误或不完善的地方,欢迎留言或私信指正,批评的话就不用了。
最后,本文会一直完善和更新。
祝大家生活愉快,早日oc。

1、var、const和let的区别

他们的区别主要体现在:

变量提升方面:var存在变量提升、但let和const不存在变量提升。

块级作用域方面:let和const具有块级作用域,而var没有。

重复声明方面:var在声明变量时是可以重复声明的,而了let和const不可以。

暂时性死区:let和const存在暂时性死区,如果不声明是无法使用的。而var可以先使用,后声明。

暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

初始值:var和let在定义时可以不设置初始值,但const必须设置初始值。而且初始值是原始数据类型的话,值不可修改,但引用数据类型的属性可以修改。

const定义引用数据类型的值可以修改,但是指针指向不会改变。

题目:

const b = [1, 2];

b = []; //报错,因为const定义常量赋值后不能重新再给常量赋值

b.push(2); //成功,输出为[1,2,2]

b[0] = 2; //成功,输出为[2,2]

b[20] = 2; //成功,输出为[1,2,<18 empty items>,2]

2、JS的事件执行机制(event loop)

首先,JS从script开始执行主线程的执行,

执行的时候,首先判断该任务是同步任务还是异步任务,

遇到同步任务放在主线程上立即执行,异步任务根据异步事件的类型,会被放到对应的宏任务和微任务队列中去。

在当前执行栈为空时,主线程会查看微任务队列是否有事件存在

如果存在,执行微任务,直至微任务的队列为空,然后取出宏任务队列中最靠前的事件,把对应的回调加入执行栈。

如果不存在微任务,就取出宏任务队列中最靠前的事件,把对应的回调加入执行栈

循环异步任务队列,直至事件全部执行完毕。

(每一个宏任务和宏任务的微任务执行完后都会对页面 UI 进行渲染。)

await 是一个让出线程的标志。await 后面的表达式会先执行一遍,将 await 后面的代码加入到 微任务队列中,这个微任务是 promise 队列中微任务,然后就会跳出整个 async 函数来继续执行后面的代码。


宏任务主要有:script(整体代码)、setTimeout、setInterval、I/O、UI 交互事件。

微任务主要有:Promise. then、Promise.catch、process.nextTick()

JS 中任务的执行顺序优先级是:主栈全局任务> 宏任务中的微任务 > 下一个宏任务。

promise构造函数是同步执行的;then方法是异步执行的微任务

当微任务中有process.nextTick时,先执行Process.nextTick

宏任务的定时器执行顺序:setTimeout——>setInterval——>setImmediate

3、为什么js是单线程的?

如果js是多线程的,那么当他们同时运行,操作一个dom时,下达两个不同的命令,dom将无法执行

扩展:

进程:是cpu分配资源的最小单位(是拥有资源和独立运行的最小单位)

线程:是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程可以有多个线程。)

浏览器是多进程的。每打开一个页面,其实就是打开了一个进程。

4、为什么js是异步的?

如果js不存在异步,只能自上而下执行,那么当上一个任务执行很长时间后,下面的任务就都会阻塞。对用户来说,页面就卡死了,这样用户体验较差。

JS主要是通过事件循环来实现异步的。

5、GUI渲染线程主要工作内容(页面渲染)

  • 解析html文档生成DOM树
  • css代码转换为cssom (css object model)
  • 结合DOM和CSSOM生成渲染树
  • 生成布局(layout)
  • 将布局绘制(paint)在屏幕上

6、浏览器解析渲染页面

浏览器解析渲染页面分为一下五个步骤:

  • 根据 HTML 解析出 DOM 树(与css解析并行,script会阻塞DOM解析和渲染)

DOM 树解析的过程是一个深度优先遍历,若遇到 script 标签,则 DOM 树的构建会暂停,直至脚本执行完毕。

  • 根据 CSS 解析生成 CSS 规则树(与HTML解析并行,不会阻塞DOM解析,但会阻塞渲染)

解析 CSS 规则树时 js 执行将暂停,直至 CSS 规则树就绪。浏览器在 CSS 规则树生成之前不会进行渲染。

  • 结合 DOM 树和 CSS 规则树,生成渲染树

DOM 树和 CSS 规则树全部准备好了以后,浏览器才会开始构建渲染树。

  • 根据渲染树计算每一个节点的信息(布局)

布局:通过渲染树中渲染对象的信息,计算出每一个渲染对象的位置和尺寸

重排(回流):在布局完成后,发现了某个部分发生了变化影响了布局,那就需要倒回去重新渲染。

  • 根据计算好的信息绘制页面

绘制阶段,系统会遍历呈现树,并调用呈现器的“paint”方法,将呈现器的内容显示在屏幕上。

重绘:某个元素的背景颜色,文字颜色等,不影响元素周围或内部布局的属性,将只会引起浏览器的重绘。

重排(回流):某个元素的尺寸发生了变化,则需重新计算渲染树,重新渲染

7、为什么JS 会阻塞 DOM 解析?

因为浏览器无法知道DOM树的内容,如果先解析了DOM,而最后js又把DOM全部删除了,那么浏览器就白解析了一次,因此需要在js执行完后,再解析DOM。

扩展:

为什么css不会阻塞DOM解析,而会阻塞DOM渲染?

在浏览器解析过程中,HTML与 CSS是并行的,所以不会阻塞DOM的解析。然后渲染的时候,渲染树必须结合DOM树和CSS树,如果CSS没有解析完成,那么就无法渲染。

为什么CSS会阻塞JS的执行?js会触发页面渲染?

如果js想要获取到DOM的最新样式,则必须先把对应的CSS加载完成后,否则获取的样式可能是错误或者不是最新的。

总结:

css不会阻塞DOM的解析,但会阻塞DOM的渲染。或者说是阻塞渲染树的生成,进而阻塞DOM的渲染。

js会阻塞DOM的解析

css会阻塞js的执行

浏览器遇到<script>标签且没有deferasync属性时会触发页面渲染

8、post和get区别

post和get作为发送请求的方法,在传递参数上,get通常将参数包含在url上,而post请求将参数包含在请求体中,因此,post请求要比get请求更安全一点。另外,因为get和post执行请求任务的不同,get一般进行的是读取的操作,如果浏览器缓存中有的话,直接从缓存中读取,不一定要和服务器进行交互。而post一般进行的是修改和删除的操作,不可避免的要和服务器进行交互,因此不能使用缓存。

传递参数区别

最直观的区别就是GET把参数包含在URL中,POST通过请求体传递参数。

GET请求只能进行url编码,而POST支持多种编码方式。

GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。

在缓存方面的区别

GET请求类似查找,用户不用每次都通过数据库访问数据,所以可以使用缓存

POST请求,一般做的是修改和删除的操作,必须和数据库交互,无法使用缓存。

底层区别

GET和POST的底层也是TCP/IP,也就是说,GET/POST都是TCP链接

GET产生一个TCP数据包;POST产生两个TCP数据包。

响应次数区别

对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);

而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

耗时区别

因为POST需要两步,时间上消耗的要多一点,看起来GET比POST更有效。

但需要注意的是:

  1. GET与POST都有自己的语义,不能随便混用。

  2. 据研究,在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。

  3. 并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。

9、闭包(重点)

闭包是

什么是闭包?

闭包就是就是“定义在一个函数内部的函数“,能够读取其他函数的内部变量。当外部函数被销毁后,return出来的这个内部函数仍旧可以访问外部函数的变量。

闭包形成的原理?

作用域链,当前作用域可以访问上级作用域中的变量。

闭包解决的问题?

优点:

​ 1、私有化变量,避免全局变量的污染

​ 2、希望一个变量长期存储在内存中(缓存变量)

缺点:

​ 1、由于闭包会使得函数中的变量都被保存在内存中,会消耗内存,引起页面卡顿。

闭包找到的是同一地址中父级函数中对应变量最终的值

闭包的用途

闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中

//经典闭包,体现了不污染全局变量的作用
function m1(){
     var x = 1;
     return function(){
          console.log(++x);
     }
}
 
m1()();   //2
m1()();   //2
m1()();   //2
 
var m2 = m1();
m2();   //2
m2();   //3
m2();   //4
//相当于m1()()的直接调用都是在开辟一个新的地址,但是把它赋给一个变量的时候,地址就不会再改变了,这个时候就会发现输出只是在递增的。
var  fn=(function(){
   var i=10;
   function  fn(){
      console.log(++i);
   }
   return  fn;
})() 
fn();   //11
fn();   //12
如何销毁闭包?

销毁闭包 : 就是把 返回出来的函数 重新赋值 例如 res = null 这样闭包就会被销毁。

10、类的创建和继承

类的创建

在ES5中,是没有类这个概念的,只是使用function模拟类的实现。

创建一个function,在这个function的prototype中添加模拟类的属性和方法。

创建一个Animals类

// 创建一个叫做Animals的构造函数,暂且称为父构造函数,里面有两个属性:name,type,一个方法:sleep
function Animals(name){
    this.name = name ||'Animals'
    this.type = ['猴子','老虎','大象']
    this.sleep = function(){
        console.log(this.name + '正在睡觉');
    }
}
//通过Animals构造函数的原型对象设置eat方法,此时Animals有2个属性和2个方法
Animals.prototype.eat = function(food){
    console.log(this.name + '正在吃'+ food);
}
类的继承(6种方法)

继承的好处:提高了代码的可复用性与可维护性。

继承的弊端:类的耦合性增强,违背了开发的原则(高内聚,低耦合)

1、原型链继承
// 创建一个叫做Cat的构造函数,暂且称为子构造函数
function Cat(){ }
//设置Cat的原型对象值为父构造函数的实例对象,即继承了父构造函数,此时Cat也有2个属性和2个方法
Cat.prototype = new Animals()
Cat.prototype.name = '猫'
//对Cat构造函数实例化,结果赋值给cat,即cat为实例化对象,同样拥有2个属性和2个方法
var cat = new Cat()
//输出各种属性和方法
console.log(cat.name);		//猫
console.log(cat.sleep());	//猫正在睡觉
console.log(cat.eat('fish'));	//猫正在吃fish		使用原型链可以访问原型对象的原型
console.log(cat instanceof Animals);	//true
console.log(cat instanceof Cat);	//true

重点:让新实例的原型等于父类的实例

特点:实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性。

缺点:1、因为Cat. prototype(即原型对象)继承了Animals实例化对象,这就导致了所有的子实例化对象都一样,共享原型对象属性和方法。2、子构造函数实例化对象无法进行参数的传递

2、构造函数继承

通过构造函数call方法进行继承

//创建一个叫做Cat的子构造函数
function Cat(name){
    Animals.call(this,name)  //函数内部通过call方法调用父级构造函数,实现继承
}
//对Cat构造函数进行多个实例化
//1
var cat1 = new Cat('小红')
cat1.type.push('猫')	//对cat1的type属性新增元素,cat2的属性不做改变,实现了独立继承
console.log(cat1.type)  //['猴子','老虎','大象','猫']
cat1.sleep()	//小红正在睡觉
cat1.eat()	//cat1.eat is not a function,因为使用构造函数继承不能调用原型对象的原型
//2
var cat2 = new Cat()
console.log(cat2.type); //['猴子','老虎','大象']

重点:使用构造函数继承,必须使用parent. call(this)实现继承

优点:1、实现实例化对象的独立性 2、可以给实例对象添加参数

缺点:1、方法都在构造函数种定义,每次实例化都得重新创建一遍方法,基本无法实现函数复用

2、call方法,仅仅调用了父级构造函数方法(prototype)的属性和方法,没有办法调用父级构造函数原型对象(prototype.prototype)的方法。

3、组合继承

利用原型链和构造函数的各自优势进行组合使用。组合函数基本满足了JS的继承,比较常用

//创建一个Cat子构造函数,传入name参数
function Cat(name){
    Animals.call(this,name)  //构造函数实现继承
}
Cat.prototype = new Animals()	//原型链实现继承
cat1 = new Cat('小黑')
cat2 = new Cat('小白')
cat1.type.push('缅因猫')  
cat2.type.push('英国短毛')  
console.log(cat1.type); //['猴子', '老虎', '大象', '缅因猫']
console.log(cat2.type); //['猴子', '老虎', '大象', '英国短毛']
cat1.Eat('fish')  //小黑正在吃fish
cat2.Eat('meat')  //小白正在吃meat

优点:1、使用原型链继承,实现原型对象方法的继承,即能够访问原型对象的原型 2、使用构造函数继承,实现属性的继承,而且可以传参数

缺点:无论什么情况下,都会调用两次父级构造函数:一次是在创建子级原型的时候,另一次是在子级构造函数内部

4、原型式继承

创建一个对象,将参数作为一个对象的原型对象

//创建一个函数fun,内部定义一个构造函数Cat
function fun(obj){
    function Cat(){}
    Cat.prototype = obj	//	将Cat的原型对象设置为参数,参数是一个对象,完成继承。
    return new Cat()	//	将Cat实例化后返回,即返回的是一个实例化对象
}
var Animals = {
    name:'敖丙'
}

var cat1 = fun(Animals)
var cat2 = fun(Animals)
console.log(cat1.name);   // 敖丙
console.log(cat2.name);   //  敖丙

优缺点:与原型链类似

5、寄生继承
function fun(obj){
    function Cat(){}
    Cat.prototype = obj
    return new Cat()
}
//在原型式继承的基础上封装一个Jisheng函数
function Jisheng(obj){
    var clone= fun(obj)
    //将fun返回的函数进行增强,新增新的Say方法
    clone.Say = function(){
    	console.log('我是新增的方法');
	}
	return clone
}
var Animals = {
	name:'敖丙'
}
//	调用Jisheng函数两次
var cat1 = Jisheng(Animals)
var cat2 = Jisheng(Animals)
cat1.Say()  //我是新增的方法
cat2.Say()	//我是新增的方法
console.log(cat1.Say=== cat2.Say);  // 对比为false,实现独立

优缺点:跟构造函数继承类似,调用一次函数就得创建一遍方法,无法实现函数复用,效率较低

这里补充一个知识点,ES5有一个新的方法Object.create(),这个方法相当于封装了原型式继承。

这个方法可以接收两个参数:第一个是新对象的原型对象(可选的),第二个是新对象新增属性,所以上面代码还可以这样:

function Jisheng(obj){
    var clone= Object.create(obj)	// ES5有一个新的方法Object.create()
    //将fun返回的函数进行增强,新增新的Say方法
    clone.Say = function(){
    	console.log('我是新增的方法');
	}
	return clone
}
var Animals = {
	name:'敖丙'
}
//	调用Jisheng函数两次
var cat1 = Jisheng(Animals)
var cat2 = Jisheng(Animals)
cat1.Say()  //我是新增的方法
cat2.Say()	//我是新增的方法
console.log(cat1.Say=== cat2.Say);  // 对比为false,实现独立
6、寄生组合继承(推荐)

利用组合继承和寄生继承各自优势组合继承方法我们已经说了,

它的缺点是两次调用父级构造函数,一次是在创建子级原型的时候,另一次是在子级构造函数内部,

那么我们只需要优化这个问题就行了,即减少一次调用父级构造函数,

正好利用寄生继承的特性,继承父级构造函数的原型来创建子级原型。

//封装一个函数JiSheng,两个参数,参数1为子级构造函数,参数2为父级构造函数
function Jisheng(son,parent){
    var clone= Object.create(parent.prototype)  // 利用Object.create(),将父级构造函数原型克隆为副本clone
    son.prototype = clone     // 将该副本作为子级构造函数的原型
    clone.constructor = son   // 给该副本添加constructor属性,因为③中修改原型导致副本失去默认的属性
}

function Parent(name){
    this.name = name
    this.type =  ['JS','HTML','CSS']
}
Parent.prototype.Say = function(name){
    console.log(this.name);
}

function Son(name){
    Parent.call(this,name)
}

Jisheng(Son,Parent)
son1 = new Son('张三')
son2 = new Son('李四')
son1.type.push('VUE')
son2.type.push('React')
console.log(son1.type); //['JS', 'HTML', 'CSS', 'VUE']
console.log(son2.type); // ['JS', 'HTML', 'CSS', 'React']
son1.Say()  //张三
son2.Say()  //李四

优缺点:

组合继承优点、寄生继承的优点,目前JS继承中使用的都是这个继承方法

11、js时间线(背下来)

客户端js时间线

1、创建document对象,开始解析web页面。创建HTMLHtmlElement对象,添加到document中。这个阶段document.readyState = ‘loading’。
2、遇到link外部css,创建线程异步加载,并继续解析文档。并发
3、遇到script外部js,并且没有设置async、defer,浏览器创建线程加载,并阻塞解析,等待js加载完成并执行该脚本,然后继续解析文档。
4、遇到script外部js,并且设置有async、defter,浏览器创建线程加载,并继续解析文档。
async属性的脚本,脚本加载完成后立即执行。(异步加载禁止使用document.write(),会清空已加载的文档流)
5、遇到img等,先正常解析dom结构,然后浏览器异步加载src, 并继续解析文档。并发
6、当文档解析完成,document.readyState = ‘interactive’。
7、文档解析完成后,所有设置有defer的脚本会按照顺序执行。(注意与async的不同)
8、document对象触发DOMContentLoaded事件,这也标志着程序执行从同步脚本执行阶段,转化为事件驱动阶段
9、当所有async的脚本加载完成并执行后、img等加载完成后,document.readyState = ‘complete’,window对象触发load事件。
10、从此,以异步响应方式处理用户输入、网络事件等。

12、如何解决异步回调地狱

什么是回调地狱?

在js中我们经常会大量使用异步回调,或者把一个函数作为另一个函数的参数。回调函数一层套一层,就逐步形成了“回调地狱”

如何解决?
1、Promise
  1. Promise 是一个构造函数,可以 new Promise() 得到一个 Promise 的实例;

  2. 在 Promise 上,有两个函数,分别叫做 resolve(成功后的回调函数) 和 reject(失败后的回调函数)

  3. 在 Promise 构造函数的 Prototype 属性上,有一个 .then() 方法,也就是说,只要是 Promise 构造函数创建的实例,都可以访问到 .then() 方法

  4. Promise 表示一个异步操作;每当我们 new 一个 Promise 的实例,这个实例,就表示一个具体的异步操作;

  5. 既然 Promise 创建的实例,是一个异步操作,那么这个异步操作的结果,只能有两种状态:

    状态1:异步执行成功了,需要在内部调用 成功的回调函数 resolve 把结果返回给调用者;

    状态2:异步执行失败了,需要在内部调用 失败的回调函数 reject 把结果返回给调用者;

    由于 Promise 的实例,是一个异步操作,所以,内部拿到操作的结果后,无法使用 return 把操作结果返回给调用者;这时候,只能使用回调函数的形式,来把成功或失败的结果,返回给调用者;

  6. 可以在 new 出来的 Promise 实例上,调用 .then() 方法,【预先】为这个 Promise 异步操作,指定 成功(resolve) 和 失败(reject) 回调函数。

**总结:**先执行同步代码,遇到异步代码就先加入队列,然后按入队的顺序执行异步代码,最后执行 setTimeout 队列的代码。

队列任务优先级promise.Trick() > promise的回调 > async > setTimeout > setImmediate

2、async/await

让我们先从async关键字说起,它被放置在一个函数前面。就像下面这样:

async function f() {
    return 1
}

函数前面的async一词意味着一个简单的事情:这个函数总是返回一个promise,如果代码中有return <非promise>语句,JavaScript 会自动把返回的这个 value 值包装成 promise 的 resolved 值。

例如,上面的代码返回resolved值为1的promise,我们可以测试一下:

async function f() {
    return 1
}
f().then(alert) // 1

所以,async确保了函数返回一个 promise,即使其中包含非 promise。够简单了吧?但是不仅仅只是如此,还有另一个关键词await,只能在async函数里使用,同样,它也很 cool。

Await

语法如下:

// 只能在async函数内部使用
let value = await promise

关键词await可以让 JavaScript 进行等待,直到一个 promise 执行并返回它的结果,JavaScript 才会继续往下执行。

以下是一个 promise 在 1s 之后 resolve 的例子:

async function f() {
    let promise = new Promise((resolve, reject) => {
        setTimeout(() => resolve('done!'), 1000)
    })
    let result = await promise // 直到promise返回一个resolve值(_)
    alert(result) // 'done!' 
}
f()

函数执行到(_)行会‘暂停’,当promise处理完成后重新恢复运行, resolve的值成了最终的result,所以上面的代码会在1s后输出'done!'

我们强调一下:await字面上使得JavaScript等待,直到promise处理完成,

然后将结果继续下去。这并不会花费任何的cpu资源,因为引擎能够同时做其他工作:执行其他脚本,处理事件等等。

这只是一个更优雅的得到 promise 值的语句,它比 promise 更加容易阅读和书写。

在真实的使用场景中,promise 在 reject 抛出错误之前可能需要一段时间,所以 await 将会等待,然后才抛出一个错误。

我们可以使用 try-catch 语句捕获错误,就像在正常抛出中处理异常一样:

async function f() {
    try {
        let response = await fetch('http://no-such-url')
    } catch (err) {
        alet(err) // TypeError: failed to fetch
    }
}
f()

总结

放在一个函数前的async有两个作用:

​ 1.使函数总是返回一个promise

​ 2.允许在这其中使用await

promise前面的await关键字能够使JavaScript等待,直到promise处理结束。然后:

​ 1.如果它是一个错误,异常就产生了,就像在那个地方调用了throw error一样。

​ 2.否则,它会返回一个结果,我们可以将它分配给一个值

他们一起提供了一个很好的框架来编写易于读写的异步代码。

有了async/await,我们很少需要写promise.then/catch,但是我们仍然不应该忘记它们是基于promise的,因为有些时候(例如在最外面的范围内)我们不得

不使用这些方法。

Promise. all也是一个非常棒的东西,它能够同时等待很多任务。

3、Generator

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同

特征:

  • function 命令与函数名之间有一个星号
  • 函数体内部使用 yield 语句定义不同的内部状态
  function * f () {
    yield 'hello'
    yield 'world'
    return '!'
  }

  let fg = f()

  // 调用遍历器对象的 next 方法,使得指针移向下一个状态,直到遇到下一条 yield 语句(或 return 语句)为止
  // done 为 true 时遍历结束
  console.log(fg.next())// {value: "hello", done: false}
  console.log(fg.next())// {value: "world", done: false}
  console.log(fg.next())// {value: "!", done: true}
  console.log(fg.next())// {value: undefined, done: true}

yield 表达式

Generator 函数内部提供了一种可以暂停执行的函数,yield 语句就是暂停标志

遍历器对象的 next 方法的运行逻辑如下:

遇到 yield 语句就是暂停执行后面的操作,并将紧跟在 yield 后的表达式的值作为返回的对象的 value 值

下次调用 next 方法继续向下执行后面的 yield 语句

直到 return 为止,将 return 的值赋值给 value,若无 return 后面的值 value 都为 undefined,此时 done 值为 true,for of 遍历停止

注意:yield 表达式只能用在 Generator 函数里面,用在其他地方都会报错

  // yield 表达式如果用在另一个表达式之中,必须放在圆括号里
  function * f () {
    console.log('hello' + yield)// error
    console.log('hello' + yield 123)// error

    console.log('hello' + (yield))// ok
    console.log('hello' + (yield 123))// ok
  }
  // yield 表达式用作函数参数或放在赋值表达式右边,可以不加括号
  function * f () {
    foo(yield 'a', yield 'b')// ok
    let input = yield// ok
  }

next 传参

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}
 
var b = foo(5);
b.next()   // { value:6, done:false }
b.next(12) // { value:8, done:false } 
b.next(13) // { value:42, done:true }

重点:next方法的参数表示上一个yield表达式的返回值!!不是函数中参数(如本例的x)的值!不可混淆!!!

13、什么是事件流

事件流:事件流描述的就是从页面中接收事件的顺序。

因为IE规定的事件流为事件冒泡,Netscape规定的事件流为事件捕获,后来ECMAScript在DOM2中对事件流进行了进一步规范。

DOM2级事件规定的事件流包括三个阶段: (1)事件捕获阶段 (2)处于目标阶段 (3)事件冒泡阶段

在这里插入图片描述

14、DOM0、DOM2

DOM0事件:

形式:1、内联模型(行内绑定),将函数名直接作为html标签中属性的属性值,但不符合W3C规定的内容与行为分离的规范。

​ 2、脚本模型(动态绑定),通过id或class选中标签,然后添加事件属性。但脚本模型同一个节点只能绑定一次同类型事件。

内联模型:

<div onclick="btnClick()">click</div>
<script>
function btnClick(){
    console.log("hello");
}
</script>

脚本模型:

<div id="btn">点击</div>
<script>
var btn=document.getElementById("btn");
btn.onclick=function(){
    console.log("hello");
}
</script>

当多个div嵌套时,绑定的点击事件会发生冒泡,DOM0事件只支持冒泡阶段

DOM2事件:

DOM2级事件处理程序:

​ addEventListener( )——添加事件侦听器

​ removeEventListener( )——移除事件侦听器

函数均有3个参数,

第一个参数是要处理的事件名

第二个参数是作为事件处理程序的函数

第三个参数是一个boolean值,默认false表示使用冒泡机制,true表示捕获机制。

var btn1 = addEventListener('click',function(){},false)	//冒泡阶段(从下往上)
var btn2 = addEventListener('click',function(){},true)	//捕获阶段(从上往下)

w3c规定了,任何发生在w3c事件模型中的事件,首是进入捕获阶段,再进入冒泡阶段。但被点击的那个元素的事件触发是按照代码的顺序执行的。

阻止冒泡:

有时候我们需要点击事件不再继续向上冒泡,我们在btn2上加上stopPropagation函数,阻止程序冒泡。

var btn1 = addEventListener('click',function(event){		//冒泡阶段(从下往上)
    event.stopPropagation(),	//阻止冒泡
},false)	

15、事件委托(重点)

什么是事件委托?

事件委托是把把原本需要绑定给子元素的事件委托给父元素,让父元素负责事件监听。

因为每绑定一个事件处理器都是有代价的,如果一个父元素有很多的子元素,要给每个子元素都绑定事件,会极大的影响页面的性能。

因此我们通过事件委托来进行优化。以此来减少内存消耗,和实现动态绑定事件。

事件委托的原理:

事件委托利用的就是冒泡的原理。

事件委托的优点:

​ 1、减小内存消耗

​ 2、动态绑定事件

事件委托举例:

在ul和li的例子中:正常情况我们给每一个li都会绑定一个监听事件,但是如果这时候li是动态渲染的,数据又特别大的时候,每次渲染后(有新增的情况)我们还需要重新来绑定,又繁琐又耗性能;这时候我们可以将绑定事件委托到li的父级元素,即ul。

var ul_dom = document.getElementsByTagName('ul')
ul_dom[0].addEventListener('click', function(ev){  
    console.log(ev.target.innerHTML)
})
target和currentTarget

上面代码中我们使用了两种获取目标元素的方式,target和currentTarget,那么他们有什么区别呢:

  • target返回触发事件的元素,不一定是绑定事件的元素
  • currentTarget返回的是绑定事件的元素

16、如何实现先冒泡后捕获

在DOM标准事件模型中,是先捕获后冒泡。这是机制问题,不可能改变的。

要实现先冒泡后捕获、可以改变的只有二者的回调函数的执行顺序。

暂缓回调函数执行的方法:

1、当业务逻辑较为简单时,可以通过定时器才实现;

2、万能的方法可以用一个局部变量来实现,在捕获的回调中去修改这个局部变量,在冒泡的回调中判断这个局部变量是否被修改。

冒泡的回调中可以是一个循环定时器,来不停地判断是否被修改,直到被修改为止。

17、图片的懒加载和预加载

预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染。

懒加载:延迟加载图片或符合某些条件时才加载某些图片。

懒加载意义:作为服务器前端的优化,减少请求数或延迟请求数

懒加载实现方式

1.第一种是纯粹的延迟加载,使用setTimeOut或setInterval进行加载延迟.

2.第二种是条件加载,符合某些条件,或触发了某些事件才开始异步下载。

3.第三种是可视区加载,即仅加载用户可以看到的区域,这个主要由监控滚动条来实现,一般会在距用户看到某图片前一定距离遍开始加载,这样能保证用户拉下时正好能看到图片。

18、谈谈你对原型和原型链的理解

每个函数对象都有一个prototype属性,称为原型,而原型的值也是一个对象,因此它也有自己的原型,这样就串联起了一条原型链。

原型链的最后是object,obj的原型是null。

每个 JS 函数都有一个属性 prototype ,该属性指向一个对象,这个对象就被称为原型对象,我们可以通过非标准属性 __proto__ 来访问一个对象的原型。

__proto__ 是非标准属性,如果要访问一个对象的原型,建议使用 ES6 新增的 Reflect.getPrototypeOf 或者 Object.getPrototypeOf() 方法。

// 纯对象的原型默认是个空对象
console.log({}.__proto__); // => {}

function Student(name, grade) {
  this.name = name;
  this.grade = grade;
}

const stu = new Student('xiaoMing', 6);
// Student 类型实例的原型,默认也是一个空对象
console.log(stu.__proto__); // => Student {}

我们可以通过对 __proto__ 属性直接赋值的方式修改对象的原型,更推荐的做法是使用使用 ES6 的 Reflect.setPrototypeOfObject.setPrototypeOf。不论哪一种方式,被设置的值的类型只能是对象或者 null,其它类型不起作用:

const obj = { name: 'xiaoMing' };
// 原型为空对象
console.log(obj.__proto__); // => {}

obj.__proto__ = 666;
// 非对象和 null 不生效
console.log(obj.__proto__); // => {}

// 设置原型为对象
obj.__proto__ = { a: 1 };
console.log(obj.__proto__); // => { a: 1 }
console.log(Reflect.getPrototypeOf(obj)); // => { a: 1 }

在原型对象上,有一个属性 constructor 指向产生这些实例的构造函数。

任何构造器都有一个 prototype 属性,默认是一个空的纯对象,所有由构造器构造的实例的原型都是指向它。

多个原型组成的就是一条原型链,在 JS 中,访问一个对象中的属性或方法,首先在对象自身中查找,如果找到则返回,否则去这个对象的原型中查找,如果没找到,就去原型的原型中查找,一直找到 Object 的原型为止。如果最终没有找到,则返回 undefined

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZHx56jOm-1650597403065)(%E9%9D%A2%E7%BB%8F.assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MTAzMDMwMg==,size_16,color_FFFFFF,t_70.png)]

//最近的一个大厂面试题:
function Page() {
  return this.hosts;
}
Page.hosts = ['h1'];
Page.prototype.hosts = ['h2'];

const p1 = new Page();
const p2 = Page();

console.log(p1.hosts);	//undefined
console.log(p2.hosts);	//error

为什么 console.log(p1.hosts) 是输出 undefiend 呢,前面我们提过 new 的时候如果 return 了对象,会直接拿这个对象作为 new 的结果,因此,p1 应该是 this.hosts 的结果,而在 new Page() 的时候,this 是一个以 Page.prototype 为原型的 target 对象,所以这里 this.hosts 可以访问到 Page.prototype.hosts 也就是 ['h2']。这样 p1 就是等于 ['h2']['h2'] 没有 hosts 属性所以返回 undefined

为什么 console.log(p2.hosts) 会报错呢,p2 是直接调用 Page 构造函数的结果,直接调用 page 函数,这个时候 this 指向全局对象,全局对象并没 hosts 属性,因此返回 undefined,往 undefined 上访问 hosts 当然报错。

19、=====的区别

==表示equality等同的意思。使用==时,如果等式两边值的类型不同的话,要先进行数据类型转换

===表示identity恒等的意思,使用===时,不需要做类型转换、如果值的类型不同,一定不相等。

简单说明使用三个等号===的判断规则:

  • 如果类型不同,就一定不相等。
  • 如果两个都是数值,并且是同一个值,那么相等;如果其中至少一个是NaN那么不相等。(判断一个值是否是NaN,只能使用isNaN()来判断)
  • 如果两个都是字符串,每个位置的字符都一样,那么相等,否则不相等

20、AJAX、axios、fetch的区别

AJAX

Gmail开发人员发现IE里面有个XMLHTTPRequest对象来请求数据时,可以实现无刷新数据请求,所以使用这个特性,进行网络数据请求,这就是AJAX的由来。

AJAX不是一个单词,他的全称是Asynchronous JavaScript and XML,就是异步的JavaScript和XML,它是一套用于创建快速动态网页的技术标准,使用步骤如下:

  1. 创建异步XMLHttpRequest对象 conet x = new XMLHttpRequest()
  2. 设置请求参数,包括请求的方法和URL等 x.open('GET','url')
  3. 发送请求 x.send()
  4. 注册事件,事件状态变更会及时响应监听
  5. 在监听里面获取并处理返回数据
x.onReadyStateChange = function(){
    if(x.readyState===4){
        if(x.status>=200&& x.status<300){
            console.log(x.response)
           }
    }
}

所以AJAX的核心就是XMLHttpRequest对象,这是一个非常早的实现方法,也是兼容性最好的,已经成为了浏览器标准,虽然我们现在都使用其它的API规范,但对象名字暂时还是用XML命名

Axios

axios是一个基于Promise的HTTP库,可以用在浏览器和node.js中,它底层还是基于XMLHttpRequest对象的,你可以认为它是一个方便的封装库,除了基础请求数据,它还增加了如下功能:

  1. 对PromiseAPI的支持
  2. 支持请求拦截和响应、转换请求数据和响应数据、取消请求
  3. 可以自动转换JSON数据
  4. 支持防御XSRF
fetch

fetch就不是XMLHttpRequest对象了,fetch是原生的js对象,也就是说,它不依赖浏览器,fetch提供了一个理解的请求替换方案,可以提供给其它技术使用。我们主要需要了解下fetch和ajax的本质区别:

  1. fetch返回的是Promise,所以如果HTTP状态码是404之类的,fetch也是成功返回的,只有在网络连接错误的情况下,才会reject
  2. fetch不发送cookies

fetch的请求写法会比AJAX简单许多,但我想,最主要的问题是,无法区分HTTP状态码了,这个在编程时还是比较常用的,所以我们目前还是使用axios比较多,而很少使用fetch

21、手写Ajax,并用Promise封装

Ajax的原理简单来说通过XmlHttpRequest对象来向服务器发送异步请求,从服务器获得数据,然后用javascript来操作DOM而更新页面

function ajax({url='',type = 'get',dataType = 'json'}){
    return new Promise((resolve,reject)=>{
        let xhr = new XMLHttpRequest()
        xhr.open(type,url,dataType)
        xhr.onReadyStateChange = function(){
            if(xhr.readyState === 4){
                if(xhr.status >=200 && xhr.status<300){
                    resolve(xhr.response)
                }
            }else{
                reject(error)
            }
        }
        xhr.send()
    })
}

22、bind(),apply,call

作用:

都可以改变函数内部的this指向。

this指向

this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象,如果有多级调用则指向最近的那个对象

调用以及传参方式

apply和call都是直接传参,而bind传参后返回的是个新的函数,只有调用才会执行。

call 、bind 、 apply 这三个函数的第一个参数都是 this 的指向对象,只是传参方式不同

bind的连续调用的问题:

在Javascript中,多次 bind() 是无效的。

更深层次的原因, bind() 的实现,相当于使用函数在内部包了一个 call / apply ,第二次 bind() 相当于再包住第一次 bind() ,故第二次以后的 bind 是无法生效的。

<script type="text/javascript">
    function Wheel(wheelSize, style){
    this.wheelSize = wheelSize;
    this.style = style;
}
function Sit(comfort, color){
    this.comfort = comfort;
    this.color = color;
}
function Model(height,width,len){
    this.height = height;
    this.width = width;
    this.len = len;
}
//apply的传参方式
function Car(wheelSize, style, comfort, color, height,width,len){
    Wheel.call(this, wheelSize, style);
    Sit.apply(this, [comfort, color]);
    Model.bind(this, height, width, len)();
    console.log(this.wheelSize,this.style,this.comfort,this.color,this.height,this.width,this.len);
    //100 '纯色' '真皮' 'black' 1800 1900 1000
}
var car = new Car(100,'纯色','真皮','black',1800,1900,1000);
</script>

23、JS的基本数据类型

JS的数据类型包括基本的数据类型和引用数据类型

基本的数据类型有Number、String、Boolean、Null和Undefined,然后在ES6中,新增了symbol作为基本数据类型。

引用数据类型常用的有:function、array、object,然后引用数据类型也统称为Object.

数据分成两大类的本质区别:基本数据类型和引用数据类型它们在内存中的存储方式不同。

关于Symbol类型:

Symbol是ES6中新提出的数据类型,主要用来定义以一个唯一的数,可以用作obj的key值。

他的创建方法为Symbol()。由于Symbol创建的数据具有唯一性,因此 Symbol()!=Symbol()

使用Symbol作为key,不能使用for获取到这个key(不可枚举),需要使用Object.getOwnPropertySymbols(obj)来获得obj为Symbol类型的key。

24、基本数据类型和引用数据类型的储存方式有什么不同?

基本数据类型:key和value都会储存在栈内存中。

引用数据类型:key存在栈内存中,value存在堆内存中,但是栈内存会提供一个引用的地址指向堆内存中的值。

25、JS怎么区分数据类型?

判断数据类型主要有4种方法:

Typeof、instanceof、Object.prototype.toString.call()、constructor、

Typeof:需要判断变量是否是number,string,boolean,undefned,function 等类型时,可以使用typeof进行判断。

instanceof:instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。能够区分Array、Object和Function,但不能判断Number、Boolean、String基本数据类型。

Object. prototype. toString. call():精确的区分数据类型。

constructor通过返回对象的构造函数来判断是什么数据类型。但因为null和undefined是无效的对象,不存在constructor,因此无法判断。

//constructor判断数据类型
const arr = [];
console.log(arr.constructor === Array); // true

const obj = {};
console.log(obj.constructor === Object); // true

const num = 1;
console.log(num.constructor === Number); // true

const str = '1';
console.log(str.constructor === String); // true

const bool = true;
console.log(bool.constructor === Boolean); // true

const nul = null;
// console.log(nul.constructor); // 报错:Uncaught TypeError: Cannot read property 'constructor' of null at <anonymous>:1:5

const undefin = undefined;
// console.log(undefin.constructor); // 报错:Uncaught TypeError: Cannot read property 'constructor' of null at <anonymous>:1:5
//Object.prototype.toString.call()判断数据类型
Object.prototype.toString.call({})              // '[object Object]'
Object.prototype.toString.call([])              // '[object Array]'
Object.prototype.toString.call(() => {})        // '[object Function]'
Object.prototype.toString.call('seymoe')        // '[object String]'
Object.prototype.toString.call(1)               // '[object Number]'
Object.prototype.toString.call(true)            // '[object Boolean]'
Object.prototype.toString.call(Symbol())        // '[object Symbol]'
Object.prototype.toString.call(null)            // '[object Null]'
Object.prototype.toString.call(undefined)       // '[object Undefined]'

26、如何区分对象和数组?

​ 1、instanceof

​ 2、Object. prototype. toString. call( )

27、怎么判断一个对象为空对象?

​ 第一种方法是使用JSON. Stringify(),将对象转换为字符串,判断是否等于字符串’{}‘ JSON. stringify({}) === ’{}‘

​ 第二种方法就是使用ES6提供的,Object. keys( )判断其长度是不是为0. Object.keys({}).length === 0

28、跨域

如果当前网址跟请求网址不同源,即协议、主机、端口不同,发送的请求即为”跨域请求“。

浏览器发送跨域请求时,会预先发送option请求到你所请求的服务器,判断服务器是否支持跨域请求。

如果支持,浏览器则继续发送正常get请求,如果不支持,则浏览器无法发送get请求。

29、如何解决跨域问题(重点)

1、JSONP实现跨域

JSONP的工作原理:就是利用

//服务器端返回如下:
handleCallback({"success": true, "user": "admin"})

//Vue axios实现jsonp:
this.$http = axios;
this.$http.jsonp('http://www.domain2.com:8080/login', {
    params: {},
    jsonp: 'handleCallback'
}).then((res) => {
    console.log(res); 
})
2、CORS跨域资源共享

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。

通过在目标域名返回 CORS 响应头来达到获取该域名的数据的目的,技术核心就是设置 response header,分为简单请求和复杂请求两种

简单请求只需要设置 Access-Control-Allow-Origin: 目标源 ,用来说明请求来自哪个源,服务器根据这个值,决定是否同意这次请求

如果服务器不许可,则返回的信息中不会包含Access-Control-Allow-Origin字段,这个错误需要onerror捕获,返回的状态码可能为200

如果服务器许可,则服务器返回的响应中会多出Access-Control-字段

CORS默认不发送cookie,需要发送cookies,则需要服务器指定Access-Control-Allow-Credentials字段,需要在ajax请求中打开withCredentials属性

复杂请求则分两步走,第一步是浏览器发起 OPTIONS 请求,第二步才是真实请求。

OPTIONS 请求需要把服务器支持的操作通过响应头来表明,如 Access-Control-Allow-Methods: POST, GET, OPTIONS,另外一个重要的响应头是 Access-Control-Allow-Credentials: true 用来表明是否接受请求中的 Cookie。

请求方法是PUT或DELETE,Content-Type字段类型是application/json

会在正式通信前,增加一次OPTIONS查询请求,预检请求

询问服务器,网页所在域名是否在服务器的许可名单中,以及可以使用那些HTTP动词和头信息字段,只有得到肯定答复,浏览器才会发出正式XMLHTTPRequest请求,否则会报错

服务器通过预检请求,以后每次浏览器正常CORS请求,都会和简单请求一样,会有一个Origin字段,服务器的回应也会有yieldAccess-Control-Allow-Origin头信息字段

3、proxy开启代理服务器

跨域限制的时候,浏览器不能跨域访问服务器。nginx反向代理和node中间件都是让请求发给代理服务器,再由代理服务器转发给要请求的服务器。因为静态页面和代理服务器是同源的,发送请求不受限制。而服务器与服务器之间不存在同源限制。

4、Websocket协议

客户端:

//创建websocket对象
let socket = new WebSocket('ws:loaclhost:3000')
//当和服务器连接成功时触发
socket.addEventListener('open',function(){console.log('和服务器连接成功')})
//给服务器发送消息
socket.send()
//获取websocket响应的消息
socket.addEventListener('message',function(e){console.log(e.data)})	//e.data存放着向服务器发送的消息内容
//websocket连接断开时触发
scoket.addEventListener('close',function(){console.log('连接被断开')})

服务端:

//引入websocket
let  ws = require('nodejs-websocket')
//创建一个server
var server = ws.createServer(function(connect){
    //收到客户端传来的信息后触发
    connect.on('text') = function(val){console.log('收到了用户传来的信息:',val)}	//val表示传入的数据
    //用户关闭网页,断开连接后触发
    connect.on('close') = function(val){console.log('用户关闭连接',val)}	//val表示关闭的指令
    //用户关闭网页后,会触发异常,因此需要设置函数接收这个异常结果,同时,服务器关闭时也会触发异常。
    connect.on('error') = function(){console.log('连接被用户关闭')}
})
server.listen('3000',function(){
    console.log('WebSocket服务启动成功')
})

30、变量提升

变量 声明提升,函数声明整体提升,

解释:函数声明后,函数整体提升至全文最前,全局可用。变量声明后,只有声明提升到全文最前,如果输出在变量声明之前,或者还未赋值,便输出变量,则输出undefined。

function a() {}
var a
console.log(typeof a) // function
// 先执行变量提升, 再执行函数提升

31、什么是防抖和节流(前端优化)

函数防抖(debounce)

函数防抖,就是在触发事件后的n秒内,函数只能执行一次,如果在n秒内又触发了事件、则会重新从头计算时间。

简单地说,当一个动作连续触发,则只执行最后一次。

栗子:坐公交,司机需要等最后一个人进入才能关门。每次进入一个人,司机就会多等待几秒再关门。

应用场景:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求
  • 手机号、邮箱验证输入检测
  • 窗口大小resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。

代码实现:

/*
防抖函数:fn:要被防抖的函数,delay:规定的时间
*/
<button id="btn">按钮</button>
<script>
    function debounce(fn,delay){
        let timer = null
        return function(){
            clearTimeout(timer)
            timer = setTimeout(()=>{
                fn()
            },delay)
        }
    }
    document.getElementById('btn').onclick = debounce(()=>{
        console.log('点击事件被触发了',Date.now());
    },1000)
</script>
函数节流(throttle)

函数节流,就是现在一个函数在规定时间内,只能执行一次。

栗子:乘坐地铁,过闸机时,每个人进入过几秒后,才允许下一个人进入。

应用场景:

  • 滚动加载,加载更多或滚到底部监听
  • 谷歌搜索框,搜索联想功能
  • 高频点击提交,表单重复提交

代码实现:

/*
节流函数:fn:要被节流的函数,delay:规定的时间
*/

//时间戳实现节流函数
  <button id="btn">按钮</button>
  <script>
    function throttle(fn,delay){
      let lastTime = 0
      return function(){
        let nowTime = new Date().getTime()
        if(nowTime - lastTime > delay){
          fn.call(this)
          lastTime = nowTime
        }
      }
    }
    document.getElementById('btn').onclick = throttle(()=>{
      console.log('点击节流事件被触发了',Date.now());
    },1000)
  </script>
//setTimeout实现节流函数
<button id="btn">按钮</button>
<script>
    function throttle(fn, delay) {
        let timer;
        return function () {
            if (timer) return	//	如果timer有值,说明还在定时器规定时间内,直接返回.
            timer = setTimeout(() => {
                fn()
                timer = null; // 在规定事件过后,执行完fn,就清空timer.此时timer为空,throttle触发可以再次进入计时器
            }, delay)
        }
    }
    document.getElementById("btn").onclick = throttle(()=>{
        console.log('触发点击事件',Date.now());   //只要过了规定的时间,就触发事件
    },1000)
</script>
相同点:

都可以通过使用setTimeout实现。

目的都是,降低回调执行频率。节省计算资源。

不同点:

函数防抖,在一段连续操作结束后,处理回调,利用 clearTimeout 和 setTimeout 实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能。

函数防抖关注一定时间连续触发,只在最后执行一次,而函数节流侧重于一段时间内只执行一次。

32、什么是立即执行函数,作用是什么

立即执行函数:

立即执行函数就是

  1. 声明一个匿名函数
  2. 马上调用这个匿名函数
(function(){alert(“这是一个立即执行函数”)})()
立即执行函数的作用

只有一个作用:立即执行函数会形成一个单独的作用域,我们可以封装一些临时变量或者局部变量,避免污染全局变量。

栗子:

<ul id=”test”>
    <li>这是第一条</li>
    <li>这是第二条</li>
    <li>这是第三条</li>
</ul>

<script>
    var liList=document.getElementsByTagName('li');
    for(var i=0;i<liList.length;i++)
    {
        (function(j){
            liList[j].onclick=function(){
                console.log(j);
            }
        })(i)
    };
</script>

33、获取html页面所有不重复的元素

let tagNames = [].slice.call(document.querySelectorAll("*")).map(dom => dom.tagName)  //tagName 属性返回元素的标签名
let res = []
tagNames.forEach(v =>{
    if(res.indexOf(v) === -1){
        return res.push(v)
    }
})
console.log(res);

34、后台获取的数据如何插到页面上

使用element-ui框架的table表单。获取后端对应的数据渲染到页面上。

使用Ajax。

在Ajax的流程的最后响应阶段,使用标签.innerHtml将response的值插入页面。

35、promise 错误的捕获

1. promise 中通过 throw 抛出错误,catch 来捕获
    let p = new Promise((resolve, reject) => {
      //resolve('成功调用')
      //reject('失败调用')
      throw 'new Promise 报错'
    })
    p.then(
      res => {
        console.log(res);
        throw '成功调用后报错'
      }, err => {
        console.log(err);
        throw '失败调用后报错'
      })
      .catch(catcherr => {
        console.log(catcherr);
      })
    // 1、在 new promise 的时候 调用 失败的函数,.then 执行失败的函数 如若 throw 抛出一个错误,.catch 是可以捕获到的
    // 2、在 then 的成功的回调函数里面 抛出错误  也是可以在 catch 捕获

2. promise 中通过 reject 抛出错误,catch 来捕获
    let p2 = new Promise((resolve, reject) => {
      // resolve('成功调用')
      reject('失败调用')
      // throw 'new Promise 报错'
    })
    p2.then(
      res => {
        console.log(res);
        return Promise.reject('成功回调的函数 里面错误')
      }, err => {
        console.log(err);
        return Promise.reject('失败回调的函数 里面错误')
      })
      .catch(catcherr => {
        console.log(catcherr);
      })
    // 1、还可以采用 return Promise.reject() 抛出错误,让 catch 接收到
    // 必须要加上 return 不然外面接收不到

3. promise 中 异步 抛出错误 ,catch 捕获
    let p3 = new Promise((resolve, reject) => {
      setTimeout(() => {
        console.log(1);
        // resolve('成功调用')
        reject('失败调用')
        // throw 'new Promise 报错'

      }, 2000);
    })
    p3.then(
      res => {
        console.log(res);
        return Promise.reject('成功回调的函数 里面错误')
      }, err => {
        console.log(2);
        console.log(err);
        return Promise.reject('失败回调的函数 里面错误')
      })
      .catch(catcherr => {
        console.log(3);
        console.log(catcherr);
      })
// 1   2  3

36、浏览器输入URL到显示到界面的过程(重点)

  • 域名解析(DNS):将域名解析成 IP 地址
  • 查找缓存:浏览器先查看浏览器缓存-系统缓存-路由缓存中是否有该地址页面,如果有则显示页面内容
  • TCP 连接:TCP 三次握手
    • 客户端发送一个带有SYN标志的数据包给服务端,服务端收到后,发送一个带有SYN/ACK的数据包给客户端传达确认信息。客户端回复带有ACK的数据包表示握手结束。连接成功。
  • 发送 HTTP 请求。
    • HTTP请求一般分为三部分,请求方法、请求头、请求体
  • 服务器处理请求并返回 HTTP 报文
  • 浏览器解析渲染页面
    • 这个过程包括解析HTML,生成DOM树,解析CSS,生成CSS树,然后结合DOM树和css树生成渲染树。然后根据渲染树计算节点的布局,然后渲染到页面上。如果渲染过程中,元素的颜色,或者背景颜色发生了变化,会引起重绘。如果元素的尺寸或位置发生了变化,则需要重新计算渲染树,进行重排。
  • 断开连接:TCP 四次挥手

37、域名解析DNS:将域名解析成 IP 地址

在浏览器输入网址后,首先要经过域名解析,因为浏览器并不能直接通过域名找到对应的服务器,而是要通过 IP 地址。

但因为IP地址比较复杂,不方便记忆,所以使用域名代替。

域名解析即DNS 协议提供通过域名查找 IP 地址,或逆向从 IP 地址反查域名的服务。

浏览器通过向 DNS 服务器发送域名,DNS 服务器查询到与域名相对应的 IP 地址,然后返回给浏览器,浏览器再将 IP 地址打在协议上,同时请求参数也会在协议搭载,然后一并发送给对应的服务器。接下来介绍向服务器发送 HTTP 请求阶段,HTTP 请求分为三个部分:TCP 三次握手、http 请求响应信息、关闭 TCP 连接。

38、TCP 连接:TCP 三次握手

首先Client端发送连接请求报文。

Server段接受连接后回复ACK报文,并为这次连接分配资源。

Client端接收到ACK报文后也向Server段发生ACK报文,并分配资源,这样TCP连接就建立了。

在客户端发送数据之前会发起 TCP 三次握手用以同步客户端和服务端的序列号和确认号,并交换 TCP 窗口大小信息。

谢希仁著《计算机网络》中讲“三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误”。

  • 客户端发送一个带 SYN=1,Seq=X 的数据包到服务器端口(第一次握手,由浏览器发起,告诉服务器我要发送请求了)
  • 服务器发回一个带 SYN=1, ACK=X+1, Seq=Y 的响应包以示传达确认信息(第二次握手,由服务器发起,告诉浏览器我准备接受了,你赶紧发送吧)
  • 客户端再回传一个带 ACK=Y+1, Seq=Z 的数据包,代表“握手结束”(第三次握手,由浏览器发送,告诉服务器,我马上就发了,准备接受吧)

39、执行期上下文

//腾讯春招笔试

function test() {
    getName = function () {
        Promise.resolve().then(() => console.log(0));
        console.log(1);
    };
    return this;
}
test.getName = function () {
    setTimeout(() => console.log(2), 0);
    console.log(3);
};
test.prototype.getName = function () {
    console.log(4);
};
var getName = function () {
    console.log(5);
};

function getName() {
    console.log(6);
}
test.getName();  // 3  setTimeout宏任务2 
getName();  // 5
test().getName();   // 1  promise.then微任务 0
getName();    // 1  promise.then微任务 0
new test.getName();    // 3 setTimeout宏任务 2
new test().getName();   // 4
new new test().getName(); // 4
// 3 5 1 1 3 4 4 0 0 2 2

40、TCP四次挥手断开连接

  • 客户端向服务器发送报文,Fin、Ack、Seq,表示已经没有数据传输了。并进入 FIN_WAIT_1 状态

(第一次挥手:由浏览器发起的,发送给服务器,我请求报文发送完了,你准备关闭吧)

  • 服务器发送报文,Ack、Seq,表示同意关闭请求。此时客户端进入 FIN_WAIT_2 状态

(第二次挥手:由服务器发起的,告诉浏览器,我请求报文接受完了,我准备关闭了,你也准备吧)

  • 服务器向客户端发送报文段,Fin、Ack、Seq,请求关闭连接。并进入 LAST_ACK 状态

(第三次挥手:由服务器发起,告诉浏览器,我响应报文发送完了,你准备关闭吧)

  • 客户端向服务器发送报文段,Ack、Seq。然后进入等待 TIME_WAIT 状态。服务器收到客户端的报文段以后关闭连接。发起方等待一定时间未收到回复,则正常关闭

(第四次挥手:由浏览器发起,告诉服务器,我响应报文接受完了,我准备关闭了,你也准备吧)

41、深拷贝和浅拷贝的区别和实现

浅拷贝和深拷贝都只针对于引用数据类型,基本数据类型都是深拷贝。

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存;但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象;

浅拷贝的方式
  • 直接赋值
  • Object. assign(target,source)(ES6新增)

直接赋值:这种方式实现的就是纯粹的浅拷贝,B的任何变化都会反映在A上。

var A={
    name:"martin",
    data:{num:10}
};
var B={};
B=A;
B.name="lucy";
console.log(A.name);    //"lucy",A中name属性已改变

Object. assign(target,source):这种方式实现的实现的是单层“深拷贝”,但不是意义上的深拷贝,对深层还是实行的浅拷贝。

这是ES6中新增的对象方法,它可以实现第一层的“深拷贝”,但无法实现多层的深拷贝。

以当前A对象进行说明

第一层“深拷贝”:就是对于A对象下所有的属性和方法都进行了深拷贝,但是当A对象下的属性如data是对象时,它拷贝的是地址,也就是浅拷贝,这种拷贝方式还是属于浅拷贝。
多层深拷贝:能将A对象下所有的属性,及时属性是对象,也能够深拷贝出来,让A和B相互独立,这种叫才叫深拷贝。

var A={
    name:"martin",
  data:{num:10}say:function(){
   console.log("hello world") 
  }
}
var B={}
Object.assign(B,A);    //将A拷贝到B
B.name="lucy";
console.log(A.name);    //martin,发现A中name并没有改变
B.data.num=5;
console.log(A.data.num);     //5,发现A中data的num属性改变了,说明data对象没有被深拷贝
深拷贝的方式
  • 递归
  • JSON对象

首先假设一个已知的对象A,然后需要把A深拷贝到B。

var A={
    name:"martin",
    data:{num:10},
    say:function () {
        console.log("say");
    }
};
var B={};

**递归赋值:**使用递归进行深拷贝时比较灵活,但是代码较为复杂;

function deepCopy(A,B) {
    for(item in A){
        if(typeof item=="object"){
            deepCopy(item,B[item]);
        }else{
            B[item]=A[item];
        }
    }
}
deepCopy(A,B);
B.data.num=5;
console.log(A.data.num);    //10,A中属性值并没有改变,说明是深拷贝

通过这种方式能实现深层拷贝,而且能自由控制拷贝是如何进行的,如:当B中有和A同名的属性,要不要重新赋值?这些都可以进行控制,但是代码相对复杂一些。

JSON. parse()和JSON. stringify:JSON对象方法实现深拷贝时比较简单,但是当拷贝对象包含方法时,方法会被丢失;

B=JSON.parse(JSON.stringfy(A));
B.name="lucy";
console.log(A.name);    //martin

通过JSON对象方法实现对象的深拷贝,我们可以看到其中B.name值的改变并没有影响A.name的值,因为A和B分别指向不同的堆内存地址,因此两者互不影响。

上述代码中B也并没有拷贝出A中的say函数,这和JSON.stringify方法的规则有关系,它在序列化的时候会直接忽略函数,因此最后A中的say函数没有被拷贝到B

42、数组的属性和方法(重点)

  1. forEach()
  2. push
  3. unshift
  4. splice
  5. pop
  6. shift
  7. toString
  8. join
  9. concat
  10. reverse
  11. sort
  12. map
  13. filter
  14. reduce
  15. indexOf

会改变原数组的方法:

​ push()

​ pop()

​ shift()

​ unshift()

​ splice()

​ sort()

​ reverse()

不改变原数组的方法:

​ filter()

​ concat()

​ join()

​ slice()

​ map()

reduce()
1、数组的遍历
  • for循环
let s = [1,2,3,4,5,6]
for(let i =0;i<s.length;i++){
    console.log(s[i])	//123456
}
for(let i in s){
    console.log(s[i])	//123456
}
  • Array. foreach( ):每个数组元素调用一次函数(回调函数)
let s = [1,2,3,4,5,6]
s.forEach((item)=>{
    console.log(item);	//123456
})
2、向数组添加元素

1、在数组末尾添加元素(push)

let s = [1,2,3,4,5,6]
s.push(7);
console.log(s);	//[1,2,3,4,5,6,7]

push有返回值,返回新添加的元素

console.log(s.push(7))	//7

2、在数组开头添加元素(unshift)

let s = [1,2,3,4,5,6]
s.unshift(7)
console.log(s); //[7, 1, 2, 3, 4, 5, 6]

unshift( )方法返回新数组的长度

let s = [1,2,3,4,5,6]
console.log(s.unshift(0))	//	7

3、在特定位置添加元素(splice)

let s = [1,2,3,4,5,6]
s.splice(2,0,3,3)	//(指定位置(从0开始),删除的数量,在指定位置前添加的元素...)
console.log(s); //[1, 2, 3, 3, 3, 4, 5, 6]

返回空数组

3、删除数组元素

​ 1、删除数组最后一个元素(pop)

let s = [1,2,3,4,5,6]
s.pop()
console.log(s);	//[1,2,3,4,5]

返回被pop的元素

console.log(s.pop());	//6

​ 2、删除数组第一个元素(shift)

let s = [1,2,3,4,5,6]
s.shift()
console.log(s); //[2,3,4,5,6]

shift()方法返回被移除的元素

let s = [1,2,3,4,5,6]
console.log(s.shift()); 	//	1

3、删除特定位置元素(splice)

let s = [1,2,3,4,5,6]
s.splice(0,3)	//(删除的起始位置,删除几位数)
console.log(s); //[4,5,6]

splice() 方法返回一个包含已删除项的数组:

let s = [1,2,3,4,5,6]
console.log(s.splice(0,2));
4、将数组转换为字符串

​ 1、toString()方法

let s = [1,2,3,4,5,6]
console.log(s.toString());	//1,2,3,4,5,6

​ 2、join()方法,可改变数组的拼接方式

let s = [1,2,3,4,5,6]
console.log(s.join("_"));	//1_2_3_4_5_6

4、concat()方法

let s = [1,2,3,4,5,6]
let l = ["a","b","c","d","e"]
let q = ["!","#","@"]
let sl = s.concat(l)
let slq = s.concat(l,q)
console.log(sl);  // [1, 2, 3, 4, 5, 6, 'a', 'b', 'c', 'd', 'e']
console.log(slq); //  [1, 2, 3, 4, 5, 6, 'a', 'b', 'c', 'd', 'e', '!', '#', '@']、
console.log(sd);  //  [1, 2, 3, 4, 5, 6, 'v', 'i', 't', 'o']
5、数组排序

sort()方法

let s = ["Banana", "Orange", "Apple", "Mango"]
let l = [40, 100, 1, 5, 25, 10];
let cars = [
    {type:"Volvo", year:2016},
    {type:"Saab", year:2001},
    {type:"BMW", year:2010}];

console.log(s.sort());  // ['Apple', 'Banana', 'Mango', 'Orange'] 按首字母排序
console.log(l.sort((a,b)=>a-b));  // 升序: [1, 5, 10, 25, 40, 100] 使用sort对数字排序,必须加回调函数
console.log(l.sort((a,b)=>b-a));  // 降序:[100, 40, 25, 10, 5, 1]  
console.log(cars.sort((a,b)=>a.year - b.year));
/*
      0: {type: 'Saab', year: 2001}
      1: {type: 'BMW', year: 2010}
      2: {type: 'Volvo', year: 2016}
      */
6、数组反转

reverse()方法

let l = [40, 100, 1, 5, 25, 10];
console.log(l.reverse()); //  [10, 25, 5, 1, 100, 40]
7、Array. map()
  • map() 方法通过对每个数组元素执行函数来创建新数组。
  • map() 方法不会对没有值的数组元素执行函数。
  • map() 方法不会更改原始数组。
//让数组中的元素*2
let s = [45, 4, 9, 16, 25]
let l = s.map((item)=>item*2)
console.log(s); //  [45, 4, 9, 16, 25]
console.log(l); //  [90, 8, 18, 32, 50]
8、Array. filter()

filter() 方法创建一个包含符合过滤条件的数组元素的新数组。

let s = [45, 4, 9, 16, 25]
let l = s.filter((item)=>item>20)
console.log(l); //  [45,25]
9 、Array. reduce()
  • reduce() 方法在每个数组元素上运行函数,以生成单个值。
  • reduce() 方法在数组中从左到右工作。reduceRight()方法从右到左工作。
  • reduce() 方法不会减少原始数组。

应用:

//求数组项之和
let arr = [45, 4, 9, 16, 25]

var sum = arr.reduce((pre,val)=>{
    return pre+val
},0)	//设置初始值为0,因此刚开始pre的值为0,val的值为4,后面依次将返回的值作为pre的值
console.log(sum)	//99	
//求数组项最大值
let arr = [45, 4, 9, 16, 25]

var maxNum = arr.reduce((pre,val)=>{
	return Math.max(pre,val)
})
console.log(maxNum)	//45
//数组去重
let arr = [45, 4, 9, 16, 25, 16, 9]
var newArr = arr.reduce((prev, cur)=>{
    prev.indexOf(cur) === -1 && prev.push(cur); //如果cur在prev中不存在,就把cur放进prev数组中,如果存在不做操作,继续迭代
    return prev;
}, []);
console.log(newArr);  //[45, 4, 9, 16, 25]
10、Array. indexOf()

在数组中搜索元素值并返回其位置,初始值从0开始。

如果未找到项目,Array.indexOf() 返回 -1。

let arr = [45, 4, 9, 16, 25, 16, 9]
var a = arr.indexOf(9)
console.log(a); //2

43、箭头函数与普通函数的区别

1、语法更加清晰,简洁
2、箭头函数不会创建自己的this
MDN上对箭头函数this的解释:箭头函数不会创建自己的this,所以它没有自己的this,它只会从自己的作用域链的上一层继承this。

箭头函数中的this实际是继承的父级作用域的this。所以,箭头函数中this的指向在它被定义的时候就已经确定了,之后永远不会改变。

3、箭头函数的this指向永远不会改变
4、.call()/.apply()/.bind()无法改变箭头函数中this的指向
5、箭头函数没有原型
let sayHi = () => {
    console.log('Hello World !')
};
console.log(sayHi.prototype); // undefined

this指向栗子

var id = 'GLOBAL';
var obj = {
  id: 'OBJ',
  a: function(){
    console.log(this.id);
  },
  b: () => {
    console.log(this.id);
  }
};

obj.a();    // 'OBJ'
obj.b();    // 'GLOBAL'	//箭头函数没有自己的this,从所处作用域链的上一层继承this
6、箭头函数不能作为构造函数使用

首先分析一下构造函数的new的作用:

  1. 创建一个空对象
  2. 再把函数中的this指向该对象
  3. 然后执行构造函数中的语句
  4. 最终返回该实例

但是因为箭头函数没有自己的this,而且this指向不能被改变,new的第二步便无法实现,所以箭头函数不能作为构造函数。

44、使用reduce实现一个compose函数

<script>
    function add(num) {
      return ++num
    }

    function multiply(num) {
      return num * 3
    }

    function divide(num) {
      return num / 2
    }
    function compose(...fns){
      return function(data){
        return fns.reverse().reduce((pre,cur)=>{
          return cur(pre)
        },data)
      }
    }
    const count = compose(add, multiply, divide)(4) //  相当于执行add(multiply(divide(4))) 
    console.log(count); //7
  </script>

45、实现数组扁平化

嵌套数组转换为一维数组:

1、递归

2、.flat(Infinity) //ES6方法

3、使用reduce实现flat方法

4、使用扩展运算符

//递归实现
let array = [1, [2, [3, [4, 5]]], 6]
let result = []
function fn(arr) {
    for (let i = 0; i < arr.length; i++) {
        if (Array.isArray(arr[i])) {
            fn(arr[i])
        } else {
            result.push(arr[i])
        }
    }
    return result
}
console.log(fn(array));	//[1,2,3,4,5,6]
//.flat()
let array = [1, [2, [3, [4, 5]]], 6]
console.log(array.flat(Infinity));	//[1,2,3,4,5,6]
//reduce实现flat方法

let arr = [1, [2, [3, [4, 5]]], 6]

function flatten(arr) {
    return arr.reduce((pre, cur) => {
        return pre.concat(Array.isArray(cur) ? flatten(cur) : cur)
    },[])
}
console.log(flatten(arr));//[1,2,3,4,5,6]
//扩展运算符

let arr = [1, [2, [3, [4, 5]]], 6]
while(arr.some(Array.isArray)){
    arr = [].concat(...arr)
}
console.log(arr);	//[1,2,3,4,5,6]
嵌套数组转换为对象:

将 [1,2,3,[4,5]] 转换成 :

{
    children:[
        {
            value:1
        },
        {
            value:2
        },
        {
            value:3
        },
        {
            children:[
                {
                    value:4
                },
                {
                    value:5
                }
            ]
        },
    ]
}
var arr = [1,2,3,[4,5]]
function convert(arr){
    let result = []
    for(let i=0;i<arr.length;i++){
        if(typeof arr[i] == 'number'){
            result.push({
                value:arr[i]
            })
        }else if(Array.isArray(arr[i])){
            result.push({
                children: convert(arr[i])
            })
        }
    }
    return result
}
let o  = convert(arr)
console.log(o);

46、null和undefined的区别

区别

undefined 表示一个变量最原始的状态值

而 null 则表示一个变量被人为的设置为空对象,而不是原始状态。

所以,在实际使用过程中,为了保证变量所代表的语义,不要对一个变量显式的赋值 undefined,当需要释放一个对象时,直接赋值为 null 即可。

什么情况下会出现undefined?
  1. 声明了一个变量,但没有赋值
  2. 访问对象上不存在的属性
  3. 函数定义了形参,但没有传递实参
  4. 使用 void 对表达式求值
null与undeifned是否相等?

null==undefinednull===undefined

null == undefined // true

null === undefined //false

undefined === undefined //true

null === null //false

原因:ES规范认为,既然null和undefined都是表示一个无效的值,那么就是相等(==)的。但是对于===因为不能转换类型。他们的类型不同,所以不相等。

47、js中有哪几种内存泄漏的情况?

  1. 意外的全局变量
  2. 闭包
  3. 未被清空的定时器
  4. 未被销毁的事件监听
  5. DOM引用

48、异步笔试题

题目的本质,就是考察setTimeout、promise、async await的实现及执行顺序,以及 JS 的事件循环的相关问题。

// 今日头条面试题
async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2')
}
console.log('script start')
setTimeout(function () {
    console.log('settimeout')
})
async1()
new Promise(function (resolve) {
    console.log('promise1')
    resolve()
}).then(function () {
    console.log('promise2')
})
console.log('script end')

答案:

script start
async1 start
async2
promise1
script end
async1 end
promise2
settimeout

异步任务的执行顺序:

同步任务—>process.nextTick—>微任务——>setTimeout——>setInterval——>setImmediate——> I/O——>UI rendering

49、如何让js不阻塞dom解析

我们在7中介绍了js为什么会阻塞dom解析,那么如何让js不阻塞dom解析呢?

第一种方法,调整顺序,实现先加载DOM,后加载JS脚本。

按照浏览器按顺序解析文档并执行脚本的特点,将JS相关代码放在HTML文档体的最后之前,这样当整个文档解析完毕后,才会执行js脚本。如果js脚本之间有依赖关系,如a依赖b,则需先加载b,注意顺序。

第二种方法,使用加载事件达到不阻塞当前DOM的目的。

document的DOMContentLoaded或window的load事件。使用加载事件,可以在加载js后不立即执行,而是等事件触发后再执行。(但js的加载仍会阻塞该script标签后的dom解析)。他们两个的区别是:DOMContentLoaded是在DOM加载完成后就会触发,不用等页面渲染完毕。而load事件必须要等页面全部所有依赖资源全部加载完成后才触发。

第三种方法,使用script标签的async和defer属性。(只用于外部引入的js)

async和defer的区别主要在于他们执行脚本的时间不同

defer会在文档解析完后再执行脚本,多个defer会按顺序执行。(推荐)

async则是在js加载好之后就会执行脚本,而且是哪个先加载好就先执行哪个。

<script async src="script.js"></script>

50、函数的this指向

1、普通函数

普通函数,谁调用这个函数,this就指向谁。

2、匿名函数

匿名函数的执行具有全局性,它的this指向window。

3、箭头函数

箭头函数没有自己的this,它的this是在定义时就已经确定下来的,且不能通过call、apply、bind更改。

箭头函数中的this指向父级作用域的执行上下文

寻找this技巧:如果想确定箭头函数的this指向,找到离箭头函数最近的普通function,与该function平级的执行上下文中的this即是箭头函数中的this

如果向上找不到父级作用域,则this指向window

let obj = {
    getThis: function () {
        return  ()=> {
            console.log(this);	//	obj
        }
    }
}
obj.getThis()(); //obj

51、link和@import的区别

<link href="index.css" rel="stylesheet">
<style text='text/css'>
	@import url(index.css)
</style>

区别:

  1. 引入方式不同:link除了可以引用css样式,还可以引用图片等资源文件,而@import只引用样式文件。
  2. 加载顺序不同:link引用css文件,在页面载入的同时加载。@import需要页面完全载入以后加载。
  3. 兼容性不同:link时XHTML标签,都兼容。@import是在css2.1提出的,低版本不支持。
  4. 对JS的支持不同:link支持用js控制dom去改变样式,而@import不支持。

52、为什么link用href获取资源 script和img用src

src用于替换当前元素,href用在当前文档和引用资源之间确认关系。

53、死锁

死锁是指两个或两个以上的进程在执行过程中,由于资源竞争而造成的阻塞现象,若无外力作用,他们都无法执行。

死锁的四个必要条件:(如果在一个系统中以下四个条件同时成立,那么就能引起死锁)

  1. 互斥条件:一个资源每次只能被一个进程使用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源把持不放。
  3. 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

54、JS获取url中的参数值(待完善)

1、获取页面的URL

🌰http://localhost:8000/#/index/cardinfo?_k=0wnq36

针对上述栗子,获取url

​ 1、window.location.href 获取整个url为字符串

var intergrityUrl = window.location.href
console.log(intergrityUrl)	//http://localhost:8000/#/index/cardinfo?_k=0wnq36

​ 2、window.location.protocol 获取url的协议部分

var protocol = window.location.protocol
console.log(protocol)	//http:

​ 3、window.location.host 获取url的主机部分

var host = window.location.host
console.log(host)	//localhost:8080

​ 4、window.location.port 获取url的端口号

var port = window.location.port
console.log(port)	//8000

​ 5、window.location.search 获取url中?后面的部分

var search = window.location.search
consolo.log(search)		//?_k=0wnq36
2、获取URL中的参数

55、map,filter,reduce方法比较

以上三个函数都会对数组进行遍历

map对元素遍历,类似于forEach操作,会返回一个新数组。

filter遍历返回的是符合过滤条件的元素,也会返回一个新数组

reduce方法有两个参数,pre和cur,使用这两个参数可以对数组进行去重、求和、拼接等操作。也会返回一个新数组。

56、display:none和visibility:hidden区别?

display:none是其元素及其子元素的占据的空间消失。即使子元素设置display:block也不会显示。

visibility:hidden是视觉上消失了,但是还占有空间,还会影响页面布局。而且visibility具有继承性,使用visibility,当子元素设置为visible时,子元素仍可以显示。

在性能上,visibility要比display的性能好,因为display在切换时会引起重排,visibility不会。

57、数组去重

数组去重主要针对的是基本数据类型。

1、使用Set

使用set后,将Set对象转换为数组的两种方法。(但无法去除重复的空对象。)

  • ES6中的扩展运算符
  • Array.from(new Set( ))
arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
[...new Set(arr)]
Array.from(new Set(arr))
//[1, 'true', true, 15, false, undefined, null, NaN, 'NaN', 0, 'a', {}, {}]
2、indexOf

遍历数组元素的索引,是否在新数组中已存在,存在则不放入新数组。(但无法去除重复的NaN和空对象。)

arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
let res = arr.reduce((pre,val)=>{
    if(pre.indexOf(val)==-1){
        pre.push(val)
    }
    return pre
},[])
//[1, 'true', true, 15, false, undefined, null, NaN, NaN, 'NaN', 0, 'a', {}, {}]
3、includes

判断一个新的数组中是否已经包含一个元素,不包含就放入新数组。(但无法去除重复的空对象。)

arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
let res = arr.reduce((pre,val)=>{
  if(!pre.includes(val)){
    pre.push(val)
  }
  return pre
},[])
//[1, 'true', true, 15, false, undefined, null, NaN, 'NaN', 0, 'a', {}, {}]
4、filter

indexOf总是返回第一个元素的下标位置,后面重复元素的位置与indexOf返回的第一个index位置不相等,因此被过滤了(无法去除重复的空对象)

arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', {}, {}];
let res = arr.filter((item,index)=>{
  return arr.indexOf(item)==index
})
console.log(res);
//[1, 'true', true, 15, false, undefined, null, NaN, 'NaN', 0, 'a', {}, {}]
5、for循环

通过两层for循环,如果遇到相等的值,则直接删除。(但不能去除重复的NaN和空对象)

arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', {}, {}];
for(let i=0;i<arr.length;i++){
  for(let j=i+1;j<arr.length;j++){
    if(arr[i]==arr[j]){
      arr.splice(j,1)
    }
  }
}
console.log(arr);
//[1, 'true', true, 15, false, undefined, null, NaN, NaN, 'NaN', 0, 'a', {}, {}]
6、JSON.stringify(终极方法)

使用JSON.stringify可以去除重复的对象元素。

function removeRepeat(arr){
    let newArr = []
    arr.forEach(item1 => {
        let isInclude = false
        newArr.forEach(item2=>{
            if(JSON.stringify(item1)===JSON.stringify(item2)){
                isInclude = true
            }
        })
        if(!isInclude){
            newArr.push(item1)
        }    
    });
    return newArr
}
let arr = [123,'webank',[1,2,3],'123',{a:1},'tencent',123,[1,2,3],{a:1}]
console.log(removeRepeat(arr));
//[ 123, 'webank', [ 1, 2, 3 ], '123', { a: 1 }, 'tencent' ]

58、为什么typeof一个null会返回Object?

主要是Javascript的历史原因,当第一版的时候,不同的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判断为 object 类型, null 的二进制表示是全 0,自然前三位也是 0,所以执行 typeof 时会返回“ object ”。

拓展:

  • 000:对象
  • 001:整型
  • 010:双精度类型
  • 100:字符串
  • 110:布尔类型

59、前端性能优化

前端优化主要可以分为两类,一类是页面级别的优化,一类是代码级别的优化。

页面级别的优化

1、减少http请求

最简单的方法就是,保持页面整洁,减少资源使用。

但如果业务需要,页面有很多的资源请求,那么可以适当的设置http缓存。如果存储允许的话缓存的越多越好,越久越好。

然后合并脚本与样式文件,或使用工具压缩文档,节省空间。也是减少http请求的方法。

另外,设置懒加载也可以减少http请求。

2、将外部脚本置底(将脚本内容在页面信息内容加载后再加载)

因为外部脚本在加载时会阻塞DOM执行,如果外部引入的脚本位置太靠前,会影响整个页面的加载速度。

3、将css放在Head中

css如果放在body中,有可能dom加载完了,css还没加载完,用户体验不好。而且,如果css太靠后,有可能会延长浏览器的渲染时间。

4、避免使用CSS表达式

background-color: expression((new Date()).getHours()%2 ? "#B8D4FF" : "#F08A00" );
代码级别的优化

1、减少作用域链的使用

2、减少重绘和重排(回流)

3、防抖和节流

60、作用域和作用域链

作用域:

作用域就是变量可以起作用的区域

JS中的作用域分为全局作用域、函数作用域和ES6新增的块级作用域

全局作用域:在函数外部声明的变量,称为全局变量,可以在页面中任何一个地方被访问。

函数作用域:函数里面声明的变量,称为局部变量,只能在函数中被访问。

块级作用域:let或const声明的变量,只能在声明的大括号内被访问。

作用域链:

作用域都会有一个上下级关系,变量的取值首先会在当前作用域中查找,如果没有,就向上级作用域查找,直到查找到全局作用域,这个查找的全部过程,叫做作用域链。

61、JS中new操作符都做了哪些事?

new 在执行时,会做下面这五件事:

  1. 开辟内存空间,在内存中创建一个新的空对象。
  2. 让构造函数中的this指向新对象。
  3. 设置新对象的proto属性指向构造函数的原型对象
  4. 执行构造函数里面的代码,给这个新对象添加属性和方法。
  5. 返回这个新对象。

62、数组与伪数组的区别?

伪数组的类型是obj,而不是真实的数组(array),无法直接调用数组方法或使用length属性有什么特殊的行为,但仍可以像遍历数组那样遍历它们,因此叫伪数组。伪数组中必须有length属性。

63、JS常用的设计模式

1、工厂模式

类似工厂,可以陈列同样的商品,做同样的事。能解决多个相似问题。但无法区别出不同的object

function Animals(args){
  let obj = new Object()
  obj.name = args.name
  obj.color = args.color
  obj.getInfo = function(){
    console.log(obj.name + ' is ' + obj.color);
  }
  return obj
}
let cat = Animals({name:'cat',color:'white'})
let dog = Animals({name:'dog',color:'brown'})

cat.getInfo()
dog.getInfo()
2、构造函数模式

在构造函数中使用this关键字,创建属性和方法,再用new关键字创建实例,通过传参实现不同的实例。可以区分出不同的object

function Car(brand,color,price){
  this.brand = brand
  this.color = color
  this.price = price
  this.showCar = function(){
    console.log(brand,color,price);
  }
}
let BMW = new Car('BMW','white','30w')
console.log(BMW);
console.log(BMW instanceof Car);  //true
3、观察者模式

观察者模式定义了一种一对多的依赖关系。当一个对象的状态发生改变时,所有依赖他的对象都将得到通知,并自动更新。

观察者模式直接对应发布者和观察者。

class Subject{
  constructor(){
    this.observers = []
  }
  add(observer){
    this.observers.push(observer)
  }
  notify(...args){
    this.observers.forEach(item =>item.update(...args))
  }
}
class Observer{
  update(...args){
    console.log(...args);
  }
}
let obj1 = new Observer()   //  观察者
let sub = new Subject()   //  目标
sub.add(obj1)   //将观察者添加进目标
sub.notify('vito')  //目标发送消息,通知观察者更新
4、发布订阅模式

发布订阅并不存在于基本的设计模式中,而是独立于观察者模式的一个强大的设计模式。

区别于观察者目标逐一通知观察者的模式,在发布订阅模式中,事件发布后,并不会逐一通知订阅者,而是发给事件调度中心,所有订阅了这个事件调度中心的订阅者都会进行更新。在发布订阅模式中,发布者和订阅者彼此不进行沟通。

class PubSub {
  constructor() {
      this.subscribers = [];
  }
   
  subscribe(topic, callback) {
      let callbacks = this.subscribers[topic];
      if (!callbacks) {
          this.subscribers[topic] = [callback];
      } else {
          callbacks.push(callback);
      }
  }

  publish(topic, ...args) {
      let callbacks = this.subscribers[topic] || [];
      callbacks.forEach(callback => callback(...args));
  }
}

// 创建事件调度中心,为订阅者和发布者提供调度服务
let pubSub = new PubSub();
// A订阅了SMS事件(A只关注SMS本身,而不关心谁发布这个事件)
pubSub.subscribe('SMS', console.log);
// C发布了SMS事件(C只关注SMS本身,不关心谁订阅了这个事件)
pubSub.publish('SMS', 'I published `SMS` event');

64、关于MVVM

前后端分离框架,Model用js表示,View负责显示

model:业务逻辑操作

view:用户界面

ViewModel:核心枢纽,关联model和view

MVVM中,model和view不发生练习,都通过ViewModel进行沟通,而且他们之间的通信都是双向的。

65、关于forEach不能使用break?

forEach不能使用break结束遍历(语法不允许,报错),那么如何结束forEach的遍历呢?

1、使用return结束
let a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 用return代替break
a.forEach((item) => {
  if (item > 5) {
    return 
  } else {
    console.log(item)
  }
})

控制台输出:

1 2 3 4 5
2、使用try…catch结构
let a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
try{
  a.forEach((item) => {
    if (item > 5) {
      throw new Error('foreach不能使用break结束')	//	使用throw抛出错误
    } else {
      console.log(item);
    }
  }) 
}catch(error){
  console.error(error);
}

控制台输出:

1 2 3 4 5
Error:foreach不能使用break结束

66、什么时候使用try…catch?

1、进行数字除法运算,分母为变量,为了防止这个变量为0时出现错误,需要使用try…catch捕获,显示分母为0

2、上传文件。为了防止留存的空间不足,上传失败,需要设置try…catch捕获,显示内存不足

3、调用接口,为了防止返回意外的值,设置try…catch捕获。

4、终止forEach遍历。

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值