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>
标签且没有defer
或async
属性时会触发页面渲染
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更有效。
但需要注意的是:
-
GET与POST都有自己的语义,不能随便混用。
-
据研究,在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。
-
并不是所有浏览器都会在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
-
Promise 是一个构造函数,可以 new Promise() 得到一个 Promise 的实例;
-
在 Promise 上,有两个函数,分别叫做 resolve(成功后的回调函数) 和 reject(失败后的回调函数)
-
在 Promise 构造函数的 Prototype 属性上,有一个 .then() 方法,也就是说,只要是 Promise 构造函数创建的实例,都可以访问到 .then() 方法
-
Promise 表示一个异步操作;每当我们 new 一个 Promise 的实例,这个实例,就表示一个具体的异步操作;
-
既然 Promise 创建的实例,是一个异步操作,那么这个异步操作的结果,只能有两种状态:
状态1:异步执行成功了,需要在内部调用 成功的回调函数 resolve 把结果返回给调用者;
状态2:异步执行失败了,需要在内部调用 失败的回调函数 reject 把结果返回给调用者;
由于 Promise 的实例,是一个异步操作,所以,内部拿到操作的结果后,无法使用 return 把操作结果返回给调用者;这时候,只能使用回调函数的形式,来把成功或失败的结果,返回给调用者;
-
可以在 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.setPrototypeOf
或Object.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
。
//最近的一个大厂面试题:
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,它是一套用于创建快速动态网页的技术标准,使用步骤如下:
- 创建异步XMLHttpRequest对象
conet x = new XMLHttpRequest()
- 设置请求参数,包括请求的方法和URL等
x.open('GET','url')
- 发送请求
x.send()
- 注册事件,事件状态变更会及时响应监听
- 在监听里面获取并处理返回数据
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对象的,你可以认为它是一个方便的封装库,除了基础请求数据,它还增加了如下功能:
- 对PromiseAPI的支持
- 支持请求拦截和响应、转换请求数据和响应数据、取消请求
- 可以自动转换JSON数据
- 支持防御XSRF
fetch
fetch就不是XMLHttpRequest对象了,fetch是原生的js对象,也就是说,它不依赖浏览器,fetch提供了一个理解的请求替换方案,可以提供给其它技术使用。我们主要需要了解下fetch和ajax的本质区别:
- fetch返回的是Promise,所以如果HTTP状态码是404之类的,fetch也是成功返回的,只有在网络连接错误的情况下,才会reject
- 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、什么是立即执行函数,作用是什么
立即执行函数:
立即执行函数就是
- 声明一个匿名函数
- 马上调用这个匿名函数
(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、数组的属性和方法(重点)
- forEach()
- push
- unshift
- splice
- pop
- shift
- toString
- join
- concat
- reverse
- sort
- map
- filter
- reduce
- 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的作用:
- 创建一个空对象
- 再把函数中的this指向该对象
- 然后执行构造函数中的语句
- 最终返回该实例
但是因为箭头函数没有自己的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?
- 声明了一个变量,但没有赋值
- 访问对象上不存在的属性
- 函数定义了形参,但没有传递实参
- 使用 void 对表达式求值
null与undeifned是否相等?
null==undefined
与 null===undefined
null == undefined // true
null === undefined //false
undefined === undefined //true
null === null //false
原因:ES规范认为,既然null和undefined都是表示一个无效的值,那么就是相等(==)的。但是对于===
因为不能转换类型。他们的类型不同,所以不相等。
47、js中有哪几种内存泄漏的情况?
- 意外的全局变量
- 闭包
- 未被清空的定时器
- 未被销毁的事件监听
- 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>
区别:
- 引入方式不同:link除了可以引用css样式,还可以引用图片等资源文件,而@import只引用样式文件。
- 加载顺序不同:link引用css文件,在页面载入的同时加载。@import需要页面完全载入以后加载。
- 兼容性不同:link时XHTML标签,都兼容。@import是在css2.1提出的,低版本不支持。
- 对JS的支持不同:link支持用js控制dom去改变样式,而@import不支持。
52、为什么link用href获取资源 script和img用src
src用于替换当前元素,href用在当前文档和引用资源之间确认关系。
53、死锁
死锁是指两个或两个以上的进程在执行过程中,由于资源竞争而造成的阻塞现象,若无外力作用,他们都无法执行。
死锁的四个必要条件:(如果在一个系统中以下四个条件同时成立,那么就能引起死锁)
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源把持不放。
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
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 在执行时,会做下面这五件事:
- 开辟内存空间,在内存中创建一个新的空对象。
- 让构造函数中的this指向新对象。
- 设置新对象的proto属性指向构造函数的原型对象
- 执行构造函数里面的代码,给这个新对象添加属性和方法。
- 返回这个新对象。
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遍历。