-
JS
数据类型
说一下js的数据类型
首先js中一共是有8种数据类型
在ES5的时候,有6种:Number、String 、Boolean、undefined、Null、Object
ES6中又新增Symbol类型。这种类型的对象永不相等,同时,谷歌67版本中出现bigInt,用来操作大整数
这8种数据类型又可以分为基本类型和引用类型,除Object外都为基本类型,Object包含Function、Array、Date等
基本类型的数据大小确定,存放在栈中;引用类型的数据需要根据情况进行特定的配置,每个空间大小不一样,所以存放在堆中,在栈里保存数据的引用地址
对于基本类型的数据可以通过typeof进行判断,【需要注意的是null属于基本类型,typeof返回的是Object】,对于引用类型数据可以通过Object.prototype.toString.call()进行判断
null和undefined的区别
- undefined表示未定义,就是应该有值,但是还没有赋值,连null的值都没有赋予;null代表空值,空引用
- null转为数值是0;undefined转为数值是NAN(not a number)
- null通过typeof判断类型时是Object;undefined的类型是undefined。
类型转换
在JS中为什么0.2+0.1>0.3?
因为0.1+0.2在计算过程中发生两次精度丢失,第一次是在0.1和0.2转成双精度二进制浮点数时,由于二进制浮点数的小数位只能存储52位,导致小数点后53位的数要进行为1则进1,为0则舍去的操作,从而造成一次精度丢失,截取之后的0.1和0.2比之前要大一些。第二次在0.1和0.2转为二进制浮点数后相加的过程中,小数位相加导致小数位多出了一位,故又进行一次舍去操作,造成精度丢失,最终导致0.1+0.2大于0.3
如何解决这个问题,使得0.1+0.2=0.3?
- 使用js中toFixed方法保留一位小数
javascript的toFixed()方法: 它是一个四舍六入五成双的诡异的方法(也叫银行家算法),"四舍六入五成双"含义:也即“4舍6入5凑偶”这里“四”是指≤4 时舍去,"六"是指≥6时进上,"五"指的是根据5后面的数字来定,当5后有数时,舍5入1;当5后无有效数字时,需要分两种情况来讲:①5前为奇数,舍5入1;②5前为偶数,舍5不进。(0是偶数) console.log((0.1 + 0.2).toFixed(1));
- 放大倍数方法:先放大倍数,随后除以相应倍数
console.log((0.1 * 10 + 0.2 * 10) / 10);//0.3
0.2+0.1不等于0.3会引发哪些bug呢
可能会引起统计页面展示错乱的bug;还有300.01优惠300元后支付金额不足0.01元等类似的bug
那为什么0.2+0.3=0.5呢?
0.2+0.3分别转为二进制进行计算:在内存中他们的尾数都是等于52位的,而他们相加必定大于52位,而他们相加又恰好前52位尾数都是0,截取后恰好也是,也就是0.5
那既然0.1不是0.1了,为什么在console.log(0.1)的时候还是0.1呢?
在console.log的时候会二进制转换为十进制,十进制再转为字符串的形式,在转换的过程中发生了取近似值,所以打印出来的是一个近似值的字符串
如何判断空对象
- 用JSON的stringify转为字符串后,跟‘{}’对比;
- 用ES6,判断Object.key(obj)返回值数值的长度是否为0
- 用ES5判断Object.getOwnPropertyNames(obj)返回的数组长度是否为0
判断数据类型有几种方法
关于数据类型的判断共有5种方法
- typeof
typeof方法不能分辨null与object类型
缺点:null为基本类型,typeof null 的值为Object,无法分辨null与Object
console.log(typeof undefined);//'undefined' console.log(typeof '10');//'String' console.log(typeof 10);//'Number' console.log(typeof false);//'Boolean' console.log(typeof Symbol());//'Symbol' console.log(typeof Function);//'function' console.log(typeof null);//'object' console.log(typeof []);//'Object' console.log(typeof {});//'object'
- instanceof
返回的是一个布尔值,通过查找原型链上是否存在一个构造函数的prototype属性,判断已知实例对象是哪个构造类型/引用类型
缺点:只能判断对象是否存在于目标对象的原型链上
obj instanceof Object;/实例obj在不在Object构造函数中
const tab = ref({ tab: 0 }) console.log(tab.value instanceof Function);//false console.log(tab.value instanceof Object);//true
- constructor
根据对象的constructor判断,返回的是创建这个对象的构造函数
console.log((2).constructor );//Number console.log((true).constructor );//Boolean console.log(('str').constructor);//String console.log(([]).constructor );//Array console.log((function() {}).constructor);//Function console.log(({}).constructor);//Object
缺点:如果修改对象的原型会指向修改后的原型
- Object.prototype.toString.call()
所有数据类型均可判断,是最好的检测方式
console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]" console.log(Object.prototype.toString.call(null)); // "[object Null]" console.log(Object.prototype.toString.call(123)); // "[object Number]" console.log(Object.prototype.toString.call("abc")); // "[object String]" console.log(Object.prototype.toString.call(true)); // "[object Boolean]"
缺点:不能细分为谁谁的实例
- Array.isArray()
判断数组,返回布尔值
console.log(Array.isArray([]));//true
instanceof原理
instanceof原理实际上就是查找对象的原型链上是否有构造函数的prototype属性
为什么不用Array.prototype.toString.call()监测数据类型
Array.prototype.toString.call() 对于undefined与null类型的数据返回报错,对于数组类型数据,返回的是展开的数组元素字符串
console.log(Array.prototype.toString.call(undefined));//报错 console.log(Array.prototype.toString.call(null));//报错 console.log(Array.prototype.toString.call(['你好', 1, true])); console.log(typeof Array.prototype.toString.call(['你好', 1, true]));
为什么typeof null是Object
因为在js中不同的对象在底层都使用二进制进行存储,在js中二进制前三位都为0的话就会被判断为object类型,然而null的二进制表示全是0,所以typeof null的判断是object
这个bug是第一版的js留下来的,因为第一版的数值是以32字节存储的,由标志位和数值组成,标志位存储的是低位的数据
其他类型的标识位:000对象、1整型、010双精度类型、100字符串、110布尔类型
==
和===
有什么区别
==是相等运算符,===是严格运算符,这两个运算符都是用来判断等式两边是否相等的,区别就是==判断相等的程度较浅,只判断数值,不判断数据类型,在比较时会自动转换数据类型;===判断相等的程度深,即判断数值,也判断数据类型,比较时不会转换数据类型
需要注意的是null、undefined之间相等比较返回值是true,严格比较返回值是false,此外与其他任何组合都为false
在进行类型转换时,
String == Number ->先将String转为Number,在比较大小
Boolean == Number ->现将Boolean转为Number,在进行比较
Object == String,Number,Symbol -> Object 转化为原始类型
NaN===NaN返回什么?
返回false ,NaN特点是唯一且不等于自身的值
手写call、apply、bind
call和apply的实现思路主要是:
- 判断是否是函数调用,如果是非函数调用抛异常
- 通过新对象(context)来调用函数,给context创建一个fn设置为需要调用的函数;结束调用完之后删除fn
bind实现思路
- 判断是否是函数调用,若非函数调用抛异常
- 返回函数:判断函数的调用方式,是否是被new出来的,new出来的话返回空对象,但是实例的__proto__指向this的prototype
- 完成函数柯里化:Array.prototype.slice.call()
什么是函数柯里化? 函数柯里化是指把接收多个参数的函数转换为接受单一参数的函数,并返回接收剩下参数的新函数
也就说函数柯里化可以把f(a,b,c)这样的多参的函数转换成f(a)(b)©这样的函数,经过转换后的函数每次依次接收单一参数,并且返回最终结果。
// 普通的add函数 function add(x, y) {return x + y} // Currying后 function curryingAdd(x) { return function (y) {return x + y} } add(1, 2) // 3 curryingAdd(1)(2) // 3
实现代码:
let obj = { name: '维生素', age: 24, sayHello: function (job, hobby) { console.log(`我叫${this.name},今年${this.age}岁。我的工作时:${job},我的爱好是:${hobby}`); } } let obj1 = { name: '小赵', age: 30, } Function.prototype.MyCall = function (context) { //先判断调用myCall是不是一个函数 //this指的是调用myCall的 if (typeof this !== 'function') { throw new TypeError('Not a Function') } //不传参默认window context = context || window //保存this context.fn = this //保存参数 //arguments是所有(非箭头)函数中都可用的局部变量,arguments对象是一个伪数组 let args = Array.from(arguments).slice(1)//Array.from 把伪数组转为数组 // 调用函数 let result = context.fn(...args) //删除函数 delete context.fn return result } Function.prototype.MyApply = function (context) { //先判断调用MyApply是不是一个函数 //this指的是调用MyApply的 if (typeof this !== 'function') { throw new TypeError('Not a Function') } let result //不传参默认window context = context || window //保存this context.fn = this //保存参数 //arguments是所有(非箭头)函数中都可用的局部变量,arguments对象是一个伪数组 if (arguments[1]) { result = context.fn(...arguments[1]) } else { result = context.fn() } //删除函数 delete context.fn return result } Function.prototype.MyBind = function (context) { //先判断调用MyApply是不是一个函数 //this指的是调用MyApply的 if (typeof this !== 'function') { throw new TypeError('Not a Function') } //保存调用的bind函数 const _this = this //保存参数---通过call方法将Array.prototype.slice对arguments对象进行操作 //---Array.prototype.slice就是对该对象使用Array类的slice方法,先将arguments转换为一个Array对象 //---之后对转换后的Array对象使用slice方法 const args = Array.prototype.slice.call(arguments, 1) //返回一个函数 return function F () { //判断是不是new出来的 if (this instanceof F) { //返回一个空对象,且使创建出来的实例的__proto__指向this的prototype,且完成函数柯里化 return new _this(...args, ...arguments) } else { //如果不是new出来的改变this指向,完成函数柯里化 return _this.apply(context, args.concat(...arguments)) } } } obj.sayHello.MyCall(obj1, '设计师', '画图') obj.sayHello.MyApply(obj1, ['设计师', '画图']) obj.sayHello.MyBind(obj1, '设计师', '画图')()
说一下call、apply、bind
首先,call、apply、bind都是改变this指向的办法
- call:调用函数fn的call属性时会将函数中的this指向修改为传入的第一个参数;将后面的参数传入给函数,作为函数的参数,并立即执行函数fn
- apply:apply的作用和call相同:修改this指向,并立即执行函数。区别在于传参形式不同,apply接受两个参数,第一个参数是要指向的this对象,第二个参数是一个数组,数组里面的元素会被展开传入函数,作为函数的参数
- bind:bind的作用是只修改this指向,但不会立即执行函数;会返回一个修改了this指向后的函数。需要调用才会执行。bind的传参和call相同
call、apply、bind的区别
- 相同点:首先三个都是用于改变this指向;其次接收的第一个参数都是this要指向的对象,而且都可以利用后续参数进行传参
- 不同点:
- 在传参方面:call与bind传参相同,多个参数依次传入;而apply只有两个参数,第二个参数为数组;
- 在函数调用方面:call与apply都是对函数进行直接调用,而bind并不会立即调用函数,而是返回一个修改this后的函数
let obj = { name: '维生素', age: 24, sayHello: function (job, hobby) { console.log(`我叫${this.name},今年${this.age}岁。我的工作时:${job},我的爱好是:${hobby}`); } } let obj1 = { name: '小赵', age: 30, } obj.sayHello('程序员', '摸鱼') obj.sayHello.call(obj1, '设计师', '画图') obj.sayHello.apply(obj1, ['设计师', '画图']) obj.sayHello.bind(obj1, '设计师', '画图')()
字面量创建对象和new创建对象有什么区别,new内部都实现了什么,手写一个new
字面量创建对象更简单,方便阅读;不需要作用域解析,速度更快
new内部:
- 创建一个新对象;
- 使新对象的__proto__指向原函数的prototype;
- 改变this指向(指向新的obj)并执行该函数,执行结果保存起来作为result;
- 判断执行函数的结果是不是null或undefined,如果是则返回之前的新对象,如果不是则返回result
function One (name, age) { this.name = name; this.age = age } let obj = new One('维生素', '18') console.log(obj);
function One (name, age) { this.name = name; this.age = age } //手写一个new function myNew (fn, ...args) { //创建一个新对象 let obj = {} //使空对象的隐式原型指向原函数的显示原型 obj.__proto__ = fn.prototype //this指向obj let result = fn.apply(obj, args) return result instanceof Object ? result : obj } let newObj = myNew(One, '小明', '18') console.log(newObj);
字面量new出来的对象和 Object.create(null)
创建出来的对象有什么区别
- 字面量和new出来的对象会继承Object的方法和属性,他们的隐式原型会指向Object的显式原型
- 而Objrct.create(null)创建出来的对象原型 为null,作为原型链的顶端,自然也没有继承Object的方法和属性
执行栈和执行上下文
什么是作用域,什么是作用域链?
- 规定变量和函数的可使用范围称为作用域,可以防止变量外泄,不同作用域下同名变量不会有冲突;在ES6之前js没有块级作用域,只有全局作用域和函数作用域。
- 作用域链:当前作用域和所有外层作用域的变量对象组成的一个链表结构。当访问一个变量时,JavaScript 引擎会按照作用域链的顺序依次查找,直到找到该变量为止。如果在全局作用域中也没找到该变量,就会抛出一个 ReferenceError 错误。
什么是执行栈,什么是执行上下文?
执行栈
- 由于js是单线程的,只能同时做一件事;当多个执行上下文存在时,就需要执行栈管理多个执行上下文,控制执行顺序
- 执行栈特点:先进后出
- 当进入一个执行环境,就会创建出它的执行上下文,然后进行压栈,当程序执行完成时,他的执行上下文就会被销毁,进行弹栈
- 栈底永远是全局环境的执行上下文,栈顶永远是正在执行函数的执行上下文
- 只有浏览器关闭的时候全局执行上下文才会弹出
执行上下文
执行上下文分为全局执行上下文、函数执行上下文与eval执行上下文
- 全局执行上下文只有一个:创建一个全局的window对象,并规定this指向window,执行js的时候就压入栈底,关闭浏览器的时候才弹出
- 函数执行上下文可以有多个:函数调用时,才会被创建;每次调用函数,都会新创建一个函数执行上下文
- eval执行上下文:js的eval()函数,执行其内部的代码时,会创建自己的执行上下文,很少用且不建议使用
执行上下文分为创建阶段、执行阶段和销毁阶段
- 创建阶段:函数环境会创建变量对象【arguments对象(并赋值)、变量声明(不赋值)、函数表达式声明(不赋值)、函数声明(并赋值)】;确定this指向;确定作用域
- 执行阶段:变量赋值、函数表达式赋值,使变量对象变成活跃对象
- 销毁阶段:执行完毕出栈,等待回收被销毁
执行上下文的特点:
- 单线程,只在主线程上运行
- 同步执行,从上向下按顺序执行
- 全局上下文只有一个,也就是window对象
- 函数执行上下文没有限制,可以有多个
- 函数每调用一次,就会产生一个新的执行上下文环境
作用域和执行上下文的区别
- 作用域:静态的,函数声明时就确定了,一旦确定就不会变化了
- 执行上下文:动态的,执行代码时动态创建,当执行结束消失
闭包
什么是闭包?闭包的作用?闭包的应用?
首先闭包是:
闭包是指有权访问另一个函数作用域中变量的函数。
闭包的作用:
- 延长局部变量的生命周期,让这些变量始终保存在内存中,不会随着函数的结束而自动销毁
- 可以在函数的外部访问到函数内部的局部变量。
- 保护:避免命名冲突
- 保存:解决循环绑定引发的索引问题
闭包的应用
- for循环中的保留i的操作
- 防抖和节流
- 函数柯里化
优点
可以避免全局变量污染
缺点
- 就是被引用的私有变量不能被销毁,增大了内存消耗,造成内存泄漏,解决方法是可以在使用完变量后手动为它赋值为null;
- 其次由于闭包涉及跨域访问,所以会导致性能损失,我们可以通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响
为什么非要访问函数内部的局部变量呢,我们不能在全局变量多定义一个变量吗?
对于定义在全局的变量需要慎重:
- 因为全局变量在整个程序运行过程中会一直存在,长期占用内存,直至程序结束才会释放内存;
- 如果某个局部作用域出现同名变量,会遮蔽或者污染全局变量
- 如果不得不用全局变量,能藏多深藏多深,可以用函数return出去,这样就是只读属性了;
全局变量过多会造成什么问题?
- 首先,全局变量是定义在全局执行上下文的,生命周期长,整个程序运行过程中会一直存在,长期占用内存,直至程序结束才会释放内存,如果全局变量过多,对于js性能是一个不小的负担,越多消耗内存越大;
- 其次,如果是多人开发,还会引发命名冲突问题;如果某个局部作用域出现同名变量,则会遮蔽或污染全局变量;
- 最后代码可读性降低,不利于排查错误和调试,后期维护难度大
原型和原型链
什么是原型?什么是原型链?如何理解
原型
原型分为隐式原型和显式原型,每个对象都有一个隐式原型,它指向自己的构造函数的显式原型。
原型链
- 原型链又叫隐式原型链,是由__proto__属性串联起来,原型链的尽头是Object.prototype;
- 所有实例的
__proto__
都指向他们构造函数的prototype
- 所有的
prototype
都是对象,自然它的__proto__
指向的是Object()
的prototype
- 所有的构造函数的隐式原型指向的都是
Function()
的显示原型- Object的隐式原型是null
继承
说一说 JS 中的常用的继承方式有哪些?以及各个继承方式的优缺点。
JS中共有7种继承方式:原型链继承、借用构造函数继承、组合继承、原型式继承、寄生式继承、寄生式组合继承、ES6-extend继承
- 原型链继承【让子类的原型等于父类的实例】
优点:子类的实例可直接访问父类原型链上、实例上的成员;相对简单
缺点:创建子类实例时,无法向父类构造函数传参;父类的引用属性会被所有子类共享,更改一个父类的引用属性,其他子类也会受影响 - 借用构造函数继承【在子类构造函数中调用父类构造函数,可以使用call(),改变父类构造函数this指向】
优点:可以在子类实例中直接向父类构造函数传参;父类的引用属性不会被子类共享
缺点:无法继承父类原型上的属性与方法 - 组合继承:【组合继承就是原型链继承和借用构造函数继承的组合。用原型链实现对原型属性和方法的继承,用借用构造函数技术来实现实例属性的继承】
优点:可以在子类实例中直接向父类构造函数传参;通过子类实例可以直接访问父类原型链和实例的成员;父类构造函数中的引用属性不会被子类共享
缺点:调用了两次父类的构造函数,造成了不必要的消耗 - 原型式继承:【利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型】
优点:不需要单独创建构造函数
缺点:子类实例不能向父类传参;父类的所有引用属性会被所有子类共享 - 寄生式继承:【在原型式继承基础上,增强对象,返回构造函数】
优点:不需要单独创建构造函数
缺点:子类实例不能向父类传参;父类的所有引用属性都会被所有子类共享 - 寄生式组合继承:【结合借用构造函数传递参数和寄生模式实现继承】
优点:只调用一次父类构造函数;子类可以向父类传参;父类方法可以复用;父类的引用属性不会被共享。缺点:代码复杂。
最成熟的方法 - ES6-extend继承:【原理还是参照寄生组合继承,基本原理是一样的】
优点:语法糖,写起来方便,比较完美
实现代码:
//原型链继承 //#region function Father () { this.age = 10 this.phone = { first: "华为", second: "小米" } } Father.prototype.getage = function () { return this.age } function Son (name, money) { this.name = name this.money = money } Son.prototype = new Father() //子类型的原型为父类型的一个实例对象 Son.prototype.constructor = Son //让子类型的原型的constructor指向子类型 Son.prototype.getmoney = function () { return this.money } var son = new Son("小米", 1000)// var son2 = new Son() console.log(son.age)//10 console.log(son.getage())//10 console.log(son.name)//小米 console.log(son.getmoney())//1000 console.log(son instanceof Son)//true console.log(son instanceof Father)//true son.phone.first = "魅族"//更改一个子类的引用属性,其他子类也会受影响 console.log(son2.phone.first)//魅族 //#endregion //借用构造函数继承 //#region function Father (name, age) { this.name = name this.age = { age: age } } Father.prototype.getname = function () { return this.name } function Son (name, age, money) { Father.call(this, name, age)//修改Father的this this.money = money } Son.prototype.getmoney = function () { return this.money } var son = new Son("小明", 12, 1000) var son2 = new Son("小李", 11, 999) console.log(son.name)//小明 console.log(son.getname())//报错 无法继承父类原型上的属性与方法 console.log(son.money)//1000 console.log(son.getmoney())//1000 console.log(son instanceof Father)//false console.log(son instanceof Son)//true console.log(son.age.age)//12 console.log(son2.age.age)//11 父类的引用属性不会被共享 //#endregion //组合继承 //#region function Father (name, age) { this.name = name this.age = { age: age } } Father.prototype.getname = function () { return this.name } function Son (name, age, money) { Father.call(this, name, age)//能够看到父类型属性 this.money = money } Son.prototype = new Father()//能看到父元素方法 Son.prototype.constructor = Son//让子类型的原型的constructor指向子类型 Son.prototype.getmoney = function () { return this.money } var son = new Son("小明", 12, 1000) var son2 = new Son("小李", 18, 1999) console.log(son.name)//小明 console.log(son.getname())//小明 console.log(son.money)//1000 console.log(son.getmoney())//1000 console.log(son instanceof Father)//true console.log(son instanceof Son)//true console.log(son.age.age)//12 console.log(son2.age.age)//18 父类构造函数中的引用属性不会被共享 //#endregion //原型式继承 //#region function object (obj) { function F () { } F.prototype = obj//对传入其中的对象执行了一次浅复制,将构造函数F的原型直接指向传入的对象。 return new F() } var person = { name: "小李", friends: ["小米", "小兰"], sayname: function () { console.log(this.name) } } var person1 = object(person) person1.name = "小王" person1.friends.push("小黑") console.log(person1.friends)//['小米', '小兰', '小黑'] person1.sayname()//小王 var person2 = object(person) person2.name = "小鱼" person2.friends.unshift("小葵") console.log(person2.friends)// ['小葵', '小米', '小兰', '小黑'] person2.sayname()//小鱼 console.log(person.friends)//['小葵', '小米', '小兰', '小黑'] //#endregion //寄生式继承 //#region function object (obj) { function F () { } F.prototype = obj return new F() } function createAnother (obj) { var clone = object(obj) clone.getname = function () { //增强对象 console.log(this.name) } return clone } var person = { name: "小李", friends: ["小米", "小兰"], } var person1 = createAnother(person) person1.friends.push("小黑") person1.name = "小红" console.log(person1.friends)// ['小米', '小兰', '小黑'] person1.getname()//小红 var person2 = createAnother(person) console.log(person2.friends)//['小米', '小兰', '小黑'] person2.getname()//小李 //#endregion //寄生式组合继承 //#region function object (obj) { function F () { } F.prototype = obj return new F() } function GetPrototype (Father, Son) { var prototype = object(Father.prototype) // 创建对象,创建父类原型的一个副本 prototype.constructor = Son // 增强对象,弥补因重写原型而失去的默认的constructor 属性 Son.prototype = prototype // 指定对象,将新创建的对象赋值给子类的原型 } function Father (name) { this.name = name this.color = ["blue", "pink", "black"] } Father.prototype.getname = function () { return this.name } function Son (name, age) { Father.call(this, name) this.age = age } GetPrototype(Father, Son) 这一句,替代了组合继承中的Son.prototype = new Father() Son.prototype.getage = function () { return this.age } var son1 = new Son("小米", 18) var son2 = new Son() son1.color.push("green") console.log(son1.getname()) //小米 console.log(son1.name) //小米 console.log(son1.color) //['blue', 'pink', 'black', 'green'] console.log(son2.color) // ['blue', 'pink', 'black'] console.log(son1 instanceof Father)//true //#endregion //ES6-extend继承 //#region class Father { constructor(name, age) { this.name = name this.age = age } call () { return "打电话" } } //子类继承父类——语法:class 子类 extends 父类 class Son extends Father { constructor(name, age, height) { //super在子类的构造方法中调用父类的构造方法 super(name, age) //this操作必须放在super后面 this.height = height } play () { console.log("玩游戏") } } var son = new Son("小王", 16, 180) console.log(son.name)//小王 console.log(son.call())//打电话 console.log(son.height)//180 //#endregion
内存泄漏、垃圾回收机制
什么是内存泄漏
JS程序中已动态分配的内存由于某种原因未释放或无法释放而引发的各种问题称为内存泄露
什么会导致内存泄漏
- 意外的全局变量
- 闭包
- 未清除的定时器
- dom清空了,还存在引用
什么是意外的全局变量
函数或全局下的变量未声明直接赋值,这个变量会挂载在全局变量下;使用this创建的变量,也会挂载在全局对象上,如果未声明变量缓存大量数据,就会导致内存泄漏
JS内存泄漏的几种解决方案
JavaScript中的内存泄漏可能会导致应用程序变得缓慢、崩溃或不稳定。解决方案:减少不必要的引用、正确清除事件监听器和定时器、避免循环引用以及使用分批处理、懒加载等技术来处理大量数据。
垃圾回收机制都有哪些策略?
垃圾回收机制通常有两种策略收集内存中无用的变量:标记清除法、引用计数法
- 标记清除法
js中最常用的垃圾收集方式就是标记清除
工作原理:是当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存
工作流程:
- 垃圾回收器,在运行的时候会给存储在内存中的所有变量都加上标记
- 去掉环境中的变量以及被环境中的变量引用的变量的标记
- 在被加上标记的会被视为准备删除的变量
- 垃圾回收器完成内存清除工作,销毁那些带标记的值并回收他们所占用的内存空间
- 引用计数法
工作原理:跟踪记录每个值被引用的次数。
工作流程:
- 当声明一个变量并给该变量赋值一个引用类型的值时候,该值的计数+1,当该值赋值给另一个变量的时候,该计数+1,当该值被其他值取代的时候,该计数-1,当计数变为0的时候,说明无法访问该值了,垃圾回收机制清除该对象
- 缺点:当两个对象循环引用的时候,引用计数无计可施。如果循环引用多次执行的话,会造成崩溃等问题。所以后来被标记清除法取代
深拷贝和浅拷贝
说一下深拷贝浅拷贝
首先深拷贝和浅拷贝目的都是为了在新的上下文环境中复用现有对象的数据
- 深拷贝
拷贝数据时将数据的所有引用结构都拷贝一份
为什么要使用深拷贝:
改变新对象时不改变源数据
方法:
- 通过递归实现深拷贝
- JSON的序列化和反序列化JSON.parse(JSON.stringify(obj))【函数和undefined会丢失】
- lodash的深拷贝函数
- 浅拷贝
如果对象中属性值的类型是原始类型,就拷贝到独立空间中,如果是引用类型,只拷贝引用地址
方法:
- for...in
- 扩展运算符
- Object.assign()
手写浅拷贝深拷贝
// ----------------------------------------------浅拷贝 let obj = { a: { a1: { a11: 1 }, a2: { a21: 123, a22: { a221: 123123 } } }, b: [1, 2, 3], c: '123', d: 123, e: true, f: undefined, g: null, h: function () { console.log('h'); }, i: new Date() } //方式1---for-in function shallowClone (o) { let obj = {} for (let i in o) { obj[i] = o[i] } return obj } let shallowObj1 = shallowClone(obj) console.log(shallowObj1); //方式2---扩展运算符 let shallowObj2 = { ...obj } console.log(shallowObj2); //方式3---Object.assign() let shallowObj3 = Object.assign({}, obj) console.log(shallowObj3); shallowObj1.b = true//基本类型,拷贝的就是基本类型的值 shallowObj1.a.a1 = 888//引用类型,拷贝的就是内存地址 console.log(obj);//如果其中一个对象改变了这个地址,就会影响到另一个对象。
// ----------------------------------------------深拷贝 let obj = { a: { a1: { a11: 1 }, a2: { a21: 123, a22: { a221: 123123 } } }, b: [1, 2, 3], c: '123', d: 123, e: true, f: undefined, g: null, h: function () { console.log('h'); }, i: new Date() } //方式1---递归 function cloneDeep (obj) { if (Object.prototype.toString.call(o) == '[object Null]' || typeof o != 'object' || Object.prototype.toString.call(o) == '[object Date]') { return obj } let result = Array.isArray(obj) ? [] : {} for (let k in obj) { result[k] = cloneDeep(obj[k]) } return result } console.log(cloneDeep(obj)); //方式2---JSON.parse(JSON.stringify(obj)) let deepObj2 = JSON.parse(JSON.stringify(obj)) //解决函数和undefined丢失情况 for (let k in obj) { if (deepObj2[k] != obj[k] || typeof obj[k] == 'undefined') { deepObj2[k] = obj[k] } } console.log(deepObj2); //方式3---lodash的深拷贝函数 import lodash from 'lodash' let deepObj3 = lodash.cloneDeep(obj) console.log(deepObj3);
单线程,同步异步
为什么JS是单线程的
js的单线程与他的用途有关,作为浏览器的脚本语言,js的主要用途是与用户互动,以及操作DOM。这就决定了它只能是单线程。如果使用多线程模式,会带来很多复杂的同步问题。
为了避免复杂性,从一诞生,js就是单线程,是js语言的核心特征。为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许js脚本创建多个线/'程,但是子线程完全受主线程控制,且不得操作DOM,所以,新标准并没有改变js单线程的本质。
如何理解同步和异步?
同步:按照代码书写顺序执行处理指令的一种模式,上一段代码完才能执行下一段代码。
异步:并行处理的方式,不必等待一个程序执行完,可以执行其他任务
常见的异步场景有:定时器、axjax请求、事件绑定
如何实现异步编程
首先引入异步编程的原因是,由于js执行环境是单线程模式,只要一个任务耗时很长,后面的任务都必须排队等待,会拖延整个程序的执行。常见的浏览器无响应(假死),往往是因为某一段js代码长时间运行,导致整个页面卡在这个地方,其他任务无法运行。而异步任务就是为了解决这个问题
异步任务好处是:提升js代码执行效率;提升js处理多任务的能力
js实现异步编程方式:
- 回调函数
- 事件监听
- 发布订阅
- Promise对象
- 生成器函数Generator/yield
- async+await
Generator是怎样使用的,以及各个阶段的变化如何
- 首先生成器是一个函数,用来返回迭代器的
- 调用生成器后不会立即执行,而是通过返回的迭代器来控制这个生成器的一步一步执行的
- 通过调用迭代器的next方法来请求一个一个的值,返回的对象有两个属性,一个是value,也就是值,另一个是done,是个布尔类型,done为true说明生成器函数执行完毕,没有可返回的值了
- done为true后继续调用迭代器的next方法,返回值的value为undefined
状态变化:
- 每当执行到yield属性的时候,都会返回一个对象
- 这时候生成器处于一个非阻塞的挂起状态
- 调用迭代器的next方法的时候,生成器又从挂起状态改为执行状态,继续上一次执行位置执行
- 直到遇到下一次yield依次循环
- 直到代码没有yield了,就会返回一个结果对象为true,value为undefined
function* fn () { // 定义一个Generator函数 yield 'hello'; yield 'world'; return 'end'; } var f1 = fn(); // 调用Generator函数 console.log(f1); // fn {[[GeneratorStatus]]: “suspended”} console.log(f1.next()); // {value: “hello”, done: false} console.log(f1.next()); // {value: “world”, done: false} console.log(f1.next()); // {value: “end”, done: true} console.log(f1.next()); // {value: undefined, done: true}
说说Promise的原理?你是如何理解Promise的?
- Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件更合理和更强大。
- 简单说就是一个容器,里面保存着未来才会结束的事件(通常是一个异步操作)的结果。
- 从语法上说,Promise是一个对象,从它可以获取异步操作的消息,并可以获取其成功或失败的结果
- 有三种状态:pending(初始状态)、fulfilled(成功)、rejected(失败);状态一旦改变,就不会再变。
- 避免了回调地狱的的问题;promise对象提供简洁的API,使得控制异步操作更加容易
Promise用法:
Promise是一个构造函数,自己身上有all、reject、resolve方法,原型上有then、catch等方法。
Promise的构造函数接收一个参数:函数需要传入两个参数:
- resolve:异步操作执行成功后的回调函数
- reject:异步操作执行失败后的回调函数
new Promise((resolve, reject) => { //进行一些异步操作 setTimeout(() => { let num = Math.ceil(Math.random() * 10)//生成1-10的随机数 【ceil向上取整;random生成[0,1)随机数】 if (num <= 5) { resolve(num)//小于5为promise状态设置为成功 } else { reject(num) } }, 200); }).then( //两个参数:成功回调、失败回调 (value) => { console.log(value); console.log(num, '一个未定义的变量'); }, (err) => { console.log(err); } ).catch((err) => { //作用1.它和then的第二个参数一样,用来指定reject的回调。用法是这样:效果和写在then的第二个参数里面一样。 //作用2.在执行resolve的回调(也就是上面then中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死js,而是会进到这个catch方法中。 console.log(err); })
try { console.log(num, '未定义变量'); } catch (err) { console.log(err.message); // 显示错误原因的信息 console.log(err); // 也可以直接输出显示整个错误对象 } finally { // 不管成功与否,都会执行 console.log('finally'); } console.log(12);//不影响代码执行
手写promise
class myPromise { constructor(executor) { this.status = 'pending'//初始状态 this.value = undefined//成功的值 this.reason = undefined//失败原因 this.successCB = []//成功回调 this.failCB = []//失败回调 //立即执行 try { executor(this.resolve, this.reject) } catch (e) { this.reject(e) } } resolve = (value) => { if (this.status !== 'pending') return//状态改变过后不能再更改 this.status = 'fulfilled'//状态改为成功 this.value = value//保存成功的值 while (this.successCB.length) {//循环调用成功回调数组中的值,调用完毕删除,直至数组为空 this.successCB.shift()() } } reject = (reason) => { if (this.status !== 'pending') return//状态更改后不能再更改 this.status = 'rejected'//状态更改为失败 this.reason = reason//保存失败原因 while (this.failCB.length) {//循环调用失败回调数组中的值,调用完毕删除,直至数组为空 this.failCB.shift()() } } then (successCB, failCB) { successCB ? successCB : value => value failCB ? failCB : reason => { throw reason } let promise1 = new myPromise((resolve, reject) => { if (this.status == 'fulfilled') {//状态为成功,执行成功回调 //queueMicrotask是新的浏览器API,可用于将同步代码转换为异步代码 //类似于setTimeout操作 queueMicrotask(() => { try { let x = successCB(this.value) //判断x的值是普通值还是promise对象 //如果是普通值,直接调用resolve //如果是promise对象,查看promise对象返回的结果 resolvePromise(promise1, x, resolve, reject) } catch (e) { reject(e) } }) } else if (this.status == 'rejected') {//状态为失败,执行失败回调 queueMicrotask(() => { try { let x = failCB(this.reason) resolvePromise(promise1, x, resolve, reject) } catch (e) { reject(e) } }) } else { this.successCB.push(() => { queueMicrotask(() => { try { let x = successCB(this.value) resolvePromise(promise1, x, resolve, reject) } catch (e) { reject(e) } }) }) this.failCB.push(() => { queueMicrotask(() => { try { let x = failCB(this.reason) resolvePromise(promise1, x, resolve, reject) } catch (e) { reject(e) } }) }) return } }) return promise1 } finally (callback) { return this.then((value) => { return myPromise.resolve(callback()).then(() => value) }, (reason) => { return myPromise.resolve(callback()).then(() => { throw reason }) }) } catch (failCB) { return this.then(undefined, failCB) } //static关键字定义静态方法 //不能在类的实例上调用静态方法,而应该通过类本身调用 static all (array) { let result = []//结果数组 let index = 0 //返回myPromise对象 return new myPromise((resolve, reject) => { //添加数据的公共方法 function addData (key, value) { result[key] = value index++ if (index == array.length) { resolve(result) } } for (let i = 0; i < array.length; i++) { let current = array[i] if (current instanceof myPromise) { //myPromise对象,成功的值添加到结果数组中,失败的话,调用reject current.then((value) => { addData(i, value) }, (reason) => { reject(reason) }) } else { //普通值 addData(i, array[i]) } } }) } static resolve (value) { if (value instanceof myPromise) return value return new myPromise(resolve => resolve(value)) } } function resolvePromise (promise1, x, resolve, reject) { if (promise1 == x) { //如果自己返回自己,就报错,并且return后,不用判断后面逻辑 return reject(new TypeError('Chaining cycle detected for promise #<Promise>')) } if (x instanceof myPromise) { //promise对象 x.then(resolve, reject) } else { resolve(x) } } new myPromise((res, rej) => { let num = Math.ceil(Math.random() * 10) if (num <= 5) { res(num) } else { rej(num) } }).then(res => { console.log(res); }, err => { console.log(err); }).finally(() => { console.log('完成'); })
async...await相对于promise的优缺点
首先,promise与async...await都是js中的异步编程的解决方案
promise:
- 优点:promise的优势是可以更好的处理并发请求的情况,有效提高代码性能。支持链式调用解决回调地狱问题;
- 缺点:promise一旦执行,无法中途取消,链式调用多个then中间不能随便跳出来;错误无法在外部被捕捉到,只能在内部进行预判处理。如果不设置回调函数,promise内部抛出的错误,不会反应到外部
async/await
- 优点:async/await使得异步代码看起来像同步代码,提高代码可读性;
对于条件语句和其他流程语句比较友好,可以直接写到判断条件里面;
处理复杂流程时,在代码清晰度方面有优势 - 缺点:无法处理promise返回的rejecte对象,要借助try...catch...;可能会导致性能问题,因为await会阻塞代码,也许之后的异步代码并不依赖于前者,仍然需要等待前者完成,失去并发性;
async/await对比promise优势:
简洁干净:使用async/await能省去写很多行代码;
错误处理:async/await能用相同的结构和好用的经典try/catch处理同步和异步错误,错误堆栈能指出包含错误的函数;
调试:async/await的一个极大优势是它更容易调试,使用async/await则无需过多的箭头函数,并且能像正常的同步调用一样直接跨过await调用
以下代码执行顺序
- 执行到await时,如果返回的是一个promise对象,await会阻塞下面代码,会先执行函数外的同步代码(在这之前先看看await中函数的同步代码,先把同步代码执行完),等待同步代码执行完之后,再回到async内部等promise状态达到fulfilled的时候再继续执行下面的代码
- 任务队列可以分为宏任务队列 和 微任务队列,当执行栈中的事件执行完毕后,js 引擎首先会判断微任务队列中是否有任务可以执行,如果有,就将微任务队首的事件压入栈中执行。队列遵循先进先出原则。
async function fn1 () { console.log(1) await fn2()//进入微任务 console.log(2) } async function fn2 () { console.log(3) } fn1() console.log(4)
const async1 = async () => { console.log('async1'); setTimeout(() => { console.log('timer1') }, 2000) await new Promise(resolve => { console.log('promise1') }) console.log('async1 end') return 'async1 success' } console.log('script start'); async1().then(res => console.log(res)); console.log('script end'); Promise.resolve(1) .then(2) .then(Promise.resolve(3)) .catch(4) .then(res => console.log(res)) setTimeout(() => { console.log('timer2') }, 1000) // 首先执行同步代码,打印出script start; // 遇到定时器timer1将其加入宏任务队列; // 之后是执行Promise,打印出promise1,由于Promise没有返回值,所以后面的代码不会执行; // 然后执行同步代码,打印出script end; // 继续执行下面的Promise,.then和.catch期望参数是一个函数,这里传入的是一个数字,因此就会发生值渗透,将resolve(1)的值传到最后一个then,直接打印出1; // 遇到第二个定时器,将其加入到微任务队列,执行微任务队列,按顺序依次执行两个定时器,但是由于定时器时间的原因,会在两秒后先打印出timer2,在四秒后打印出timer1。
console.log('1'); setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') }) }) process.nextTick(function() { console.log('6'); }) new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { console.log('8') }) setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') }) }) //1---7---6---8---2---4---3---5---9---11---10---12
当执行栈中的事件执行完毕后,js 引擎首先会判断微任务队列中是否有任务可以执行,如果有,就将微任务队首的事件压入栈中执行。队列遵循先进先出原则。
Promise的回调与setTimeout的执行顺序
首先,Promise与setTimeout本质上都是异步操作,前者属于异步微任务(microtask),后者则是异步宏任务(macrotask)。
在JS的编译与执行过程中,当进行到异步操作时,会优先处理微任务,直到所有微任务与同步程序处理完毕才会开始处理宏任务。而与程序中代码的书写顺序无关(即使setTimeout写在Promise前也是如此。)
因此,二者的实际执行顺序是:先执行Promise的回调,后执行setTimeout。
需要注意的是:微任务队列上创建的微任务,仍会阻碍后方将要执行的宏任务队列;由微任务创建的宏任务,会被丢在异步宏任务队列中执行
async function fn1 () { console.log("fn1 start"); await fn2(); console.log("fn1 end"); await fn2() console.log("fn2 end"); } async function fn2 () { console.log("fn2"); } console.log("start"); setTimeout(() => { console.log("setTimeout"); }); fn1(); console.log("end");
宏任务和微任务都有哪些
js宏任务有:script整体代码、定时器、setImmediate、异步Ajax请求、DOM事件回调、I/O操作等
js微任务有:process.nextTick、Object.observe、MutationObserver、Promise的回调
宏任务和微任务都是怎样执行的
先执行同步代码,遇到异步宏任务则将异步宏任务放入宏任务队列中,遇到异步微任务则将异步微任务放入微任务队列中,当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行,微任务执行完毕后再将异步宏任务从队列中调入主线程执行,一直循环直至所有任务执行完毕。
例题
//例题1 setTimeout(function () { console.log('1') }); new Promise(function (resolve) { console.log('2'); resolve(); }).then(function () { console.log('3') }); console.log('4'); new Promise(function (resolve) { console.log('5'); resolve(); }).then(function () { console.log('6') }); setTimeout(function () { console.log('7') }); function bar () { console.log('8') foo() } function foo () { console.log('9') } console.log('10') bar() //2---4---5---10---8---9---3---6---1---7 //例题2 setTimeout(() => { console.log('1'); new Promise(function (resolve, reject) { console.log('2'); setTimeout(() => { console.log('3'); }, 0); resolve(); }).then(function () { console.log('4') }) }, 0); console.log('5'); setTimeout(() => { console.log('6'); }, 0); new Promise(function (resolve, reject) { console.log('7'); resolve(); }).then(function () { console.log('8') }).catch(function () { console.log('9') }) console.log('10'); //5 7 10 8 1 2 4 6 3
变量提升
变量和函数怎么进行提升的?优先级是怎么样的?
- 变量提升就是将变量提升到他所在作用域的最开始的部分,在变量定义之前就可以访问这个变量,值为undefined
- 函数提升是将函数提升到他所在作用域的最开始的部分,在函数声明语句之前,可以直接调用此函数,值为函数定义
js中创建函数有两种方式:函数声明和函数表达式。只有函数声明才存在函数提升- 函数提升优先级高于变量提升,且不会被同名变量声明时覆盖,但是会被变量赋值后覆盖
//函数声明 console.log(fn1);//函数 fn1()//函数声明式 function fn1 () { console.log('函数声明式'); } //函数表达式 console.log(fn2);//undefined // fn2()//报错 //代码执行之前,会把fn2提升到最前面,值为undefined,不是一个函数 //以函数的形式来进行调用时将会报错。 var fn2 = function () { console.log("函数表达式"); }
例题
//例题1 var a = 4 function fn () { console.log(a) var a = 5 } fn() //undefined //例题2 function a() {} var a; console.log(typeof a) //function function a() {} var a = 1; console.log(typeof a) //number //函数提升优先级高于变量提升,且不会被同名变量声明时覆盖,但是会被同名变量赋值后覆盖。 //例题3 console.log(typeof a) function a() {} var a = 1; //function //函数提升的优先级高于变量提升。 //例题4 console.log(v1); var v1 = 100; function foo() { console.log(v1); var v1 = 200; console.log(v1); } foo(); console.log(v1); //undefined---undefined---200---100 //例题5 console.log(person) console.log(fun) var person = 'jack' console.log(person) function fun () { console.log(person) var person = 'tom' console.log(person) } fun() console.log(person) //undefined---函数---jack---undefined---tom---jack
var、let、const有什么区别
let和const是ES6新增的声明变量的关键词,之前声明变量的关键词是var
- ES6之前没有块级作用域概念,所以var不存在块级作用域,而let与const具有块级作用域
- 由于var中存在变量提升,在声明变量之前访问该变量的值为undefined,而let和const不存在变量提升,在声明该变量之前,该变量都是不可用的,访问该变量的话会报错,语法上称为暂时性死区
- 在全局下声明变量,var会将该变量添加为全局对象的属性,let和const则不会,只在局部起作用
- var声明的变量是允许重复声明的,后声明的变量会覆盖之前声明的变量,let与const同一作用域下不允许重复声明
- 声明变量时,var与let可以只声明不赋值,而const声明的变量必须设置初始值
- let创建的变量是可以更改指针指向。而const声明的变量不允许改变指针的指向
ES6中的块级作用域解决了ES5中内层变量可能覆盖外层变量;以及用来计数的循环变量泄漏为全局变量的问题,如果没有特殊情况,尽可能使用let与const替代var进行变量声明
模块化
为什么需要使用模块化?
- 首先js在没有原生支持模块化时,开发者常使用命名空间和立即执行函数等方式来实现模块化。然而,命名空间容易造成命名冲突,立即执行函数又存在代码冗余和性能问题等。
- 模块化是将系统分离成独立功能的模块,需要什么功能就加载什么模块,简单来说模块化就是一种管理代码的方法,将代码分割为不同的模块或文件,并通过特定的方式来管理他们之间的依赖关系和导出关系。
- 好处是:代码结构更清晰;避免了命名空间的冲突;更好的分离,实现了按需加载;提高了代码的复用性,易于维护和拓展。
都有哪几种方式可以实现模块化,各有什么特点?
- 命名空间:通过为全局对象添加属性来避免命名冲突,例如将模块的所有函数和变量放在一个对象中,以避免与全局命名空间冲突;这种方式会导致命名空间变得臃肿,难以维护
- IIFE模式:使用立即执行函数表达式来创建私有作用域,从而避免命名冲突。也称模块模式,它使用一个匿名函数来封装模块,并返回一个公共接口。
特点:创建块级(私有)作用域,避免变量污染和命名冲突;IIFE 中定义的任何变量和函数,都会在执行结束时被销毁,减少闭包占用的内存。- CommonJS模块化:使用require()导入模块,exports导出模块,使得模块可以在不同的环境中使用。nodeJS采用CommonJS标准来实现模块化
- AMD模块化:支持异步加载模块,使用define()来定义模块,使用require()来加载模块
- ES6模块化:ES6引入了原生的模块化系统,可以使用import和export语句导入导出模块。需要注意的是,import和export命令只能在模块的顶层,在代码块中会报错,因为ES Module需要在编译时期进行模块静态优化,import和export命令会被JavaScript引擎静态分析,先于模块内其他语句执行,这种设计有利于编译器提高效率,但也导致无法在运行时加载模块(动态加载)
JS模块包装格式有哪些?
- CommonJS:同步运行,不适合前端
- AMD:异步运行,RequireJS规范,
- CMD:异步运行,seajs规范
CommonJS、AMD、CMD、UMD、ES6 Module都有什么区别
- AMD/CMD\UMD适合前端 异步执行
AMD和CMD的差别是:
AMD是依赖前置(把依赖放在前面)、提前执行(即使没有用到某个模块,也会提前 执行)
CMD依赖就近、延时执行(用到的时候在声明依赖)- ESM:使用export、export default来导出模块,使用import来引入模块
CommonJS和ES6模块化的区别主要在于:
1. CommonJS是运行时加载,是一个对象,ES6是编译时加载,是一个代码块;
2. CommonJS是同步加载模块,ES6是异步加载模块;
3. CommonJS是对值的浅拷贝,ES6是对值的引用,而且不可修改(直接地址不可修改,类似于const)
4. CommonJS的this指向当前模块,ESM的this指向undefined
require和import的区别?
- require是CommonJS语法,import是ES6语法
- require只在后端服务器支持,import在高版本浏览器及Node中都可以支持
- require引入的是原始导出值的复制,import则是导出值的引用
- require是运行时动态加载,import是静态编译
- require默认调用不是严格模式,import默认调用严格模式
exports和module.exports有什么区别?
- 模块中exports指向module.exports
commonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,她的exports属性是对外的接口。加载某个模块,其实是加载该模块的module.exports属性,并且node为每个模块提供了一个exports变量指向module.exports。 - exports只能使用.语法来向外暴露内部变量:如exports.xxx=xxx
module.exports既可以通过.语法,也可以直接赋值一个对象- exports可以不断的添加属性或方法,多个module.exports导出时会覆盖,以最后一个为准
- 当同时使用时,只会输出最后一个module.exports的内容
export default 和export之间的区别
- 首先他们都属于ES6里面的语法,均可用于导出常量、函数、文件、模块等
- 一个文件或模块中export可以有多个,export default仅有一个
- 通过export导出,在导入时需要知道export抛出的变量名或函数名,不能自定义名字,要加{};export default则不需要知道export抛出的变量名或函数名也不需要加{},导入时可自定义名字
- export直接导出或先定义后导出都可以,export default只能先定义后导出
其他
箭头函数和普通函数的区别?箭头函数可以当作构造函数new吗?
- 箭头函数是在ES6中新增的,写法上比普通函数更加简洁,但是没有普通函数的一些特性
- 箭头函数没有自己的this,它的this始终指向创建时所在作用域指向的对象,call、apply、bind等方法不能改变箭头函数中this指向
- 箭头函数不能作为构造函数使用,没有prototype属性,也没有自己的arguments对象,没有yield属性,不能作为生成器Generator使用
箭头函数中访问的arguments实际上获得的是它外层函数的arguments值,可以使用ES6中的rest参数代替,const fn=(...args)=>args console.log(fn(1, 2, 3));
手写ajax
ajax是用于实现局部网页异步刷新的技术。ajax的实现主要包括四步:创建核心对象XMLHttpRequest;利用open方法打开与服务器的连接;利用send方法发送请求(‘post请求时,需额外设置请求头);监听服务器响应,接收返回值
function ajax (options) {
let url = options.url
//如果有请求方式全部转为小写,默认为get请求
const method = options.method.toLocaleLowerCase() || 'get'
//默认为异步执行true
const async = options.async != false
const data = options.data
const xhr = new XMLHttpRequest()
//设置请求超时
if (options.timeout && options.timeout > 0) {
xhr.timeout = options.timeout
}
return new Promise((resolve, reject) => {
xhr.ontimeout = () => reject && reject('请求超时')
xhr.onreadystatechange = () => {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
resolve && resolve(xhr.responseText)
} else {
reject && reject()
}
}
}
xhr.onerror = err => reject && reject(err)
let paramArr = []
let encodeData
if (data instanceof Object) {
for (let key in data) {
// 参数拼接需要通过 encodeURIComponent 进行编码
paramArr.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
}
encodeData = paramArr.join('&')
}
if (method == 'get') {
//检测 url 中是否已存在 ? 及其位置
const index = url.indexOf('?')
if (index == -1) url += '?'
else if (index !== url.length - 1) url += '&'
url += encodeData
}
xhr.open(method, url, async)
if (method == 'get') xhr.send(null)
else {
xhr.setRequestHeader('Content-type', 'application/x-www-urlencoded;charset=UTF-8')
xhr.send(encodeData)
}
})
}
ajax({
url: 'your request url',
method: 'get',
async: true,
timeout: 1000,
data: {
test: 1,
aaa: 2
}
}).then(
res => console.log('请求成功: ' + res),
err => console.log('请求失败: ' + err)
)
什么是防抖节流,手写防抖节流
- 首先,防抖与节流是针对快速连续触发和不可控的高频触发问题,两者都是为了减少触发频率,提高性能,避免资源浪费。
- 防抖:用于将用户的操作行为触发转换为程序行为触发,防止用户操作的结果抖动。防抖是延迟函数执行,n秒内事件没有再次触发时再去执行回调函数,如果n秒内事件又被触发,则重新开始计时
- 节流:用于用户在于页面交互时控制事件发生的频率。n秒内频繁触发的事件,只有一次生效
- 防抖使用场景:输入框中频繁的输入内容,搜索或者提交信息;频繁的点击按钮,触发某个事件,监听浏览器滚动事件,完成某些特定操作,用于缩放浏览器的resize事件;节流使用场景:监听页面滚动事件,鼠标移动事件,用户频繁点击按钮操作,游戏中的一些设计
//防抖
//设置第三个参数,控制是否需要首次立即执行
function myDebounce (fn, delay, immediate = false) {
//1.创建一个变量,保存上一次的定时器
let timer = null
//定义变量,用于记录状态
let isInvoke = false
//2.触发事件时真正执行的函数--接收可能传入的参数
const _debounce = function (...args) {
//取消上一次的定时器
if (timer) clearTimeout(timer)
//第一次执行不需要延迟
if (!isInvoke && immediate) {
fn.apply(this, args)
isInvoke = true
return
}
//延迟执行传入fn的回调
timer = setTimeout(() => {
//使用显示绑定apply方法--将参数传给fn
fn.apply(this, args)
//3.函数执行完成后,将timer重置
timer = null
//重置isInvoke
isInvoke = false
}, delay)
}
//增添一个取消函数
_debounce.cancel = function () {
if (timer) clearTimeout(timer)
//取消也需要重置
timer = null
isInvoke = false
}
return _debounce
}
//节流
//设置第三个参数控制是否立即执行
function myThrottle (fn, interval,immediate=true) {
//定义变量,保存开始时间
let startTime = 0
const _throttle = function (...args) {
//获取当前时间
const nowTime = new Date().getTime()
//控制是否立即执行
if(!immediate&&startTime==0){
startTime=nowTime
}
//计算需要等待的时间
const waitTime = interval - (nowTime - startTime)
//当等待时间小于等于0,执行回调
if(waitTime<=0){
fn.apply(this,args)
//并让开始时间重新赋值为当前时间
startTime=nowTime
}
}
return _throttle
}
函数柯里化原理
柯里化又称部分求值,一个柯里化的函数首先会接受一些参数,接收了这些参数后,该函数并不会立即求值,而是继续返回另一个函数,刚才传入的参数,在函数形成的闭包中被保存起来,等到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
function add() { var args = Array.prototype.slice.call(arguments) var adder = function () { args.push(...arguments) return adder } adder.toString = function () { return args.reduce((prev, curr) => { return prev + curr }, 0) } return adder } let a = add(1, 2, 3) let b = add(1)(2)(3) console.log(a) console.log(b) console.log(add(1, 2)(3)); console.log(Function.toString) // --------普通函数转为柯里化函数------ function createCurry(fn, args = []) { return function () { let _args = args.concat(...arguments) // 函数的length是js函数对象的一个属性,函数的length代表形参的个数(即有多少必传参数) if (_args.length < fn.length) { return createCurry.call(this, fn, _args) } return fn.apply(this, _args) } } function add(a, b, c) { return a + b + c; } var _add = createCurry(add); console.log(_add(1, 2, 3)); console.log(_add(1)(2, 3)); console.log(_add(1)(2)(3));
什么是requestAnimationFrame?
- requestAnimationFrame是浏览器用于定时循环操作的API,是一个浏览器的宏任务,用法上与setTimeout很相似,只是不需要设置时间间隔而已。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下次重绘之前执行。主要用途是按帧对网页进行重绘,让各种网页动画效果,能有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果,
- 优点:requestAnimationFrame会把每一帧中所有的DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧跟浏览器的刷新频率,因此不会丢帧现象,也不会导致动画出现卡顿。充分利用了显示器的刷新机制,比较节省系统资源。
- requestAnimationFrame是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,当页面被激活时,动画就会从上次停留的地方继续执行,有效节省了CPU、GPU和内存使用量
- 缺点:requestAnimationFrame目前还存在兼容性问题,不同的浏览器需要带不同的前缀,需要降级对其进行封装;requestAnimationFrame是在主线程上完成,如果主线程任务过多的话,动画效果会大打折扣
var s = 0 function f(DOMHighResTimeStamp) { s++ console.log(s); console.log(DOMHighResTimeStamp); if (s < 99) { window.requestAnimationFrame(f) } } window.requestAnimationFrame(f)
requestAnimationFrame跟setTimeout的对比
- setTimeout通过设置时间间隔来不断改变图像位置,达到动画效果。在js中setTimeout任务被放进了异步任务队列中,只有等主线程上任务执行完之后,才回去检查该队列的任务是否需要开始执行,这个等待时间造成了原本设定的动画时间间隔不准
- 由于setTimeout设置的动画只是在内存中对图像属性进行改变,这个变化必须等到屏幕下次刷新时才会被更新到屏幕上,如果两者步调不一致,可能会导致丢帧现象
- 而requerstAnimationFrame采用系统时间间隔,保证最佳的绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果
js常见的设计模式
- 设计模式是什么?====> 设计模式的概念比较抽象,是程序员在开发中面临的一般问题的解决方案,这些解决方案是大家经过相当长的一段时间的试验和错误总结出来的
- js常见的设计模式是:工厂模式、单例模式、适配器模式、装饰模式、代理模式、观察者模式、迭代器模式
- 工厂模式:使用函数封装逻辑来代替new创建一个对象,并且这个函数像工厂制作一样,批量制作属性相同但是指向不同的实例对象
- 单例模式:不管创建多少个对象都只有一个实例
- 适配器模式:通过包装一层的方式来解决两个不兼容的接口相互合作的问题
- 装饰模式:不改变已有接口,给对象添加额外的功能
- 代理模式:有时候一个对象不适合或者不能直接引用另一个对象,而代理对象可以在两个对象之间起到中介的作用。例如ES6的proxy,就是在目标对象之前架设一层拦截代理;前端的跨域请求也可以借用代理实现。
- 观察者模式:定义了对象间一对多的依赖关系,当目标对象的状态发生改变时,所有依赖它的对象都会得到通知
- 迭代器模式:提供一种方法顺序访问集合对象中的各个元素,而不暴露该对象的内部表示
//工厂模式
function Animal(o) {
var instance = new Object()
instance.name = o.name
instance.age = o.age
instance.getAnimal = function () {
return "name:" + instance.name + " age:" + instance.age
}
return instance
}
var cat = Animal({name:"cat", age:3})
console.log(cat);
//单例模式
var Single = (function () {
var instance = null
function Single(name) {
this.name = name
}
return function (name) {
if (!instance) {
instance = new Single(name)
}
return instance
}
})()
var oA = new Single('hi')
var oB = new Single('hello')
console.log(oA);
console.log(oB);
console.log(oB === oA);
//适配器模式
// 已有的地图接口
var googleMap = {
show: function () {
console.log("开始渲染谷歌地图");
},
};
var baiduMap = {
display: function () {
console.log("开始渲染百度地图");
},
};
// 已有的渲染接口
var renderMap = function (map) {
if (map.show instanceof Function) {
map.show();
}
};
// 目的
renderMap(googleMap); // 开始渲染谷歌地图
renderMap(baiduMap); // 无效
// 适配器
var baiduMapAdapter = {
show: function () {
return baiduMap.display();
},
};
renderMap(googleMap); // 开始渲染谷歌地图
renderMap(baiduMapAdapter); // 开始渲染百度地图
//装饰模式
function readonly(target, key, descriptor) {
descriptor.writable = false;
return descriptor;
}
class Test {
@readonly
name = "yck";
}
let t = new Test();
t.yck = "111"; //不可改变
//代理模式
class Car {
drive() {
return "driving";
}
}
class Driver {
constructor(age) {
this.age = age;
}
}
class CarProxy {
constructor(driver) {
this.driver = driver;
}
drive() {
return this.driver.age < 18 ? "too young to drive" : new Car().drive();
}
}
const driver = new Driver(18);
const carProxy = new CarProxy(driver).drive(); // driving
//观察者模式
// 目标者类
class Subject {
constructor() {
this.observers = []; // 观察者列表
}
// 添加
add(observer) {
this.observers.push(observer);
}
// 删除
remove(observer) {
let idx = this.observers.findIndex((item) => item === observer);
idx > -1 && this.observers.splice(idx, 1);
}
// 通知
notify() {
for (let observer of this.observers) {
observer.update();
}
}
}
// 观察者类
class Observer {
constructor(name) {
this.name = name;
}
// 目标对象更新时触发的回调
update() {
console.log(`目标者通知我更新了,我是:${this.name}`);
}
}
// 实例化目标者
let subject = new Subject();
// 实例化两个观察者
let obs1 = new Observer("前端开发者");
let obs2 = new Observer("后端开发者");
// 向目标者添加观察者
subject.add(obs1);
subject.add(obs2);
// 目标者通知更新
subject.notify();
// 输出:
// 目标者通知我更新了,我是前端开发者
// 目标者通知我更新了,我是后端开发者
//迭代器模式
const each = function(arg, callback) {
for(var i = 0; i < arg.length; i++) {
callback(i, arg[i])
}
}
each([1, 2, 3], function(i, item) {
console.log('i', i, item)
})
移动端点击300ms延迟问题和移动端点击穿透问题
- 问题
点击穿透:页面有A、B两个元素。B在A之上,B有touchstart事件作用为隐藏B,当点击B时,B被隐藏,随后A触发点击事件;在移动端事件执行touchstart>touchend>click,click有300ms延迟,当touchstart事件隐藏B之后,隔300ms触发click事件,此时B已经隐藏,所以该事件派发到A,触发A的点击事件,解决点击穿透根本方案是解决300ms延迟的问题
300ms延迟:双击缩放,当用户点击一次屏幕时,浏览器需要判断用户具体操作为点击操作还是双击缩放操作。因此会有300ms的等待时间。- 解决方案:
1.禁止双击缩放:添加meta标签,表面页面不可缩放,禁用默认的双击缩放行为并且去掉300ms的点击延迟。2.更改默认的视口宽度:设置浏览器的视口宽度等于设备浏览器的视窗宽度,设置meta标签,浏览器可以认为已经对浏览器做过适配和优化了,故移动端浏览器自动禁掉了双击缩放行为,并且去掉了300ms的点击延迟。<meta name="viewport" content="user-scalable=no"> <meta name="viewport" content="initial-scale=1,maximum-scale=1">
<meta name="viewport" content="width=device-width">
3.CSS touch-action属性:CSS的touch-action属性设置为none表示在该元素上的操作不会触发用户代理的任何默认行为,就无需进行300ms延迟判断
以上三种方案会有兼容性问题
4.FastClick插件库:专解决300ms延迟问题,原理是在检测到touched事件的时候,通过DOM自定义事件立即模拟出一个click事件,并把浏览器在300ms后的click事件阻止掉
事件委托
事件委托(事件代理),利用事件冒泡,把子元素的事件都绑定到父元素上。好处是:提高性能;减少事件绑定;从而减少内存占用
事件流(事件传播)
事件流:从页面中接收事件的顺序(即事件传播)
事件流存在三个阶段:事件捕获阶段;处于目标阶段、事件冒泡阶段
捕获阶段,事件从window开始向下触发元素事件;目标阶段:事件已到达目标元素;冒泡阶段:事件由最具体元素接收,然后逐级向上传播至window
注意:js代码只能执行捕获或冒泡其中一个阶段
e.target和e.currentTarget的区别
e.target是触发事件的对象,e.currentTarget是绑定该事件的对象
setTimeout第三个参数是什么?
setTimeout函数有三个参数:
1.fn需要执行的函数【必传】2.time时间间隔【非必传】3.param fn函数的参数setTimeout((...args) => console.log(...args), 0, { name: '维生素' }); // { name: '维生素' }
什么是暂时性死区?
就是在一个作用域中,去访问一个已经存在,但是是不可获取的的变量,这个时候就会报错,这就是暂时性死区。
x = '123'//报错 let x = 1 typeof y//报错 let y = 123
使用let定义了x,在定义之前使用x就会报错;暂时性死区意味着typeof也不是绝对安全的操作
js遍历数组的方法
循环遍历、for of方法、for in遍历、forEach遍历、map映射、filter过滤、reduce高阶函数、every、some
代码实现
let arr = [1, 2, 3, 4, 5, 6] // 循环遍历 for (let i = 0; i < arr.length; i++) { console.log(arr[i]); } // for of方法 for (let item of arr) { console.log(item); } // for in遍历 for (let key in arr) { console.log(arr[key]); } // forEach遍历 arr.forEach((item, index, self) => { console.log(item) }) // map映射 arr.map((item, index, self) => { console.log(item); }) // filter过滤 arr.filter((item, index, self) => { console.log(item); }) // reduce高阶函数 arr.reduce((total, item, index, self) => { console.log(item); }, 0) // some arr.some((item, index, self) => { console.log(item); })
let arr = [1, 2, 3, 4, 5, 6] //every let result = arr.every((item, index, self) => { if (item < 8) return item }) console.log(result);
方法理解:
for-in和for-of的区别:
- 遍历数组时:for-in获取的是遍历项的索引值,for-of获取的是遍历项的value值
- 遍历对象时:for-in获取的是对象的key值,for-of不能直接遍历对象,通过Object.keys(obj)处理后进行遍历
- for-in更适合遍历对象,for-of更适合遍历数组
数据大时,使用底层的for循环更好,map(),set(),for in,for of方法说到底都是对于for循环的封装, 单从性能角度考虑,远不如for循环优秀
map()与forEach()的区别:
- 共同点:不改变原数组;对于空数组不会执行函数,函数参数都支持三个参数;匿名函数指向都为window;只能遍历数组
- 区别:
1.返回值:map有返回值,返回值为一个length和原数组一致的新数组,内容可能包含undefined、null等;forEach没有返回值,返回结果为undefined,一般也可用来遍历修改原数组
2.map处理速度比forEach快,而且返回一个新的数组,方便链式调用其他数组新方法
every()与some()的区别:
- 共同点:不改变原数组;对于空数组不会执行函数,只能遍历数组
- 区别:
1. 结束循环方式:every只要有一个不符合条件,终止循环,返回false;some只要有一个符合条件,终止循环,返回true
2. 函数内部没有return语句时:some还会执行内部代码,返回值为false;every内部代码只执行一次,返回值为false
数组可以改变原数组的方法
改变原数组的方法有:shift、unshift、pop、push、reverse、sort、splice
shift--头部删除,返回被删除元素、unshift--头部添加,返回新数组长度;
pop--尾部删除,返回被删除元素、push--尾部添加,返回新数组长度reverse--颠倒数组,返回颠倒后的原数组
splice--添加、删除、替换,返回值是被删除元素的数组,没有删除则返回空数组
sort--数组自带的排序方法,返回排序后的原数组
方法理解:
//sort排序 let arr = [1, 0, 22, 52, 88, 6, 7] let newArr = arr.sort((a, b) => a - b) console.log(newArr);
- sort()函数为可选参数,如果不传,字符数组元素将按照ASCII字符顺序进行排序,数字数组排序时将数字转为字符串在进行排序
排序函数有两个参数:a代表后一个元素,b代表前面的元素,函数返回值如果是正值,顺序不变,如果为负值,则调转顺序
返回值为排序后的原数组- splice(start,length,item):起始位置索引号,需要删除个数,替换数组元素
不改变原数组的方法
不改变原数组方法reduce、slice、filter、every、some、indexOf、lastIndexOf、find、findIndex、forEach、join、includes、map、isArray、toString、concat
方法理解:
concat()方法:合并数组,返回的是一个浅拷贝的值
参数为值或者数组(可以传对象)【可选】let arr1 = [1, 2] let arr2 = [3, 4] let arr3 = [5, 6] console.log(arr1.concat() == arr1);//说明concat是浅拷贝 console.log(arr1.concat(arr2));//合并两个数组 console.log(arr1.concat(arr2, arr3));//合并多个数组 console.log(arr1.concat(arr2, 9));//合并数组+值 console.log(arr1.concat(arr2, { a: 1, b: 2 }));//合并数组+对象
Array.includes():判断数组中是否包含指定的值,返回布尔值,arr.includes(查找的元素值)
let arr = [10, 20, 30, 40, 50]; console.log(arr.includes(10)); //true console.log(arr.includes(10, 1));//false
splice和slice
- lice切片的意思,根据传入的起始和终止下标,获取该范围数组。
- splice可根据传入参数个数不同实现删除、插入操作,直接操作原数组。第1个参数为起始下标,第2个为删除个数,第3个为要增加的数据。
数组去重
- 双重for循环
- for循环indexOf
- sort排序
- Set
- filter
let arr = [1, 3, 3, 5, 6, 6, 9]
//双重for循环
//创建一个新的空数组
let newArr = []
for (let i = 0; i < arr.length; i++) {
//设置一个开关,如果是true,就存进去,不是就不存
let flag = true
for (let j = 0; j < newArr.length; j++) {
//原数组和新数组作比较,如果一致,开关变为false
arr[i] == newArr[j] ? flag = false : true
}
flag ? newArr.push(arr[i]) : newArr
}
console.log(newArr);
//for循环+indexOf
function newArrFn (arr) {
let newArr = []
for (let i = 0; i < arr.length; i++) {
newArr.indexOf(arr[i]) == -1 ? newArr.push(arr[i]) : newArr
}
return newArr
}
console.log(newArrFn(arr));
// sort排序
function newArrFn (arr) {
arr = arr.sort()
let newArr = []
for (let i = 0; i < arr.length; i++) {
arr[i] == arr[i - 1] ? newArr : newArr.push(arr[i])
}
return newArr
}
console.log(newArrFn(arr));
// Set
function newArrFn (arr) {
return ([...new Set(arr)])
}
console.log(newArrFn(arr));
// set+Array.from
function newArrFn (arr) {
//new Set方法,返回是一个类数组,需要结合Array.from,转为真实数组
return (Array.from(new Set(arr)))
}
console.log(newArrFn(arr));
// filter+indexOf
function newArrFn (arr) {
return Array.prototype.filter.call(arr, function (item, index) {
return arr.indexOf(item) == index
})
}
console.log(newArrFn(arr));
//filter+indexOf
function newArrFn (arr) {
return arr.filter((item, index) => {
return arr.indexOf(item) == index
})
}
console.log(newArrFn(arr));
localstorage怎么存储图片
创建一个canvas对象,把图片保存在canvas中,然后canvas对象toDataUrl,在把dataurl数据存储在localstorage中
或者使用blob二进制流存储,canvas对象toBlob
localStorage并不适合存储有大量图片和网页的缓存,其最大空间为5M
如何实现大文件上传
问题:由于大文件上传是一个请求中,要上传大量的数据,会导致整个过程耗时过长;而且通常情况下前后端都会对一个请求的时间进行限制,很容易超时导致上传失败,上传失败后需要重新开始上传。
解决:采用分片上传的技术,将大文件分成一个个小文件【切片】,将切片进行上传,等到后端接收到所有切片后,再将切片合成大文件。
好处:请求时可以并发执行,每个请求时间缩短;如果某个请求发送失败了,也不需要全部重新发送。
具体实现:
- 读取文件:使用input接受大文件(input类型为file),使用input.files获取File对象。
- 创建切片:使用files.slice进行分割分块上传
- 上传切片:数据处理,采用FormData对象来进行整理数据;并发请求,每个切片都分别作为一个请求,使用Promise.all()保证所有的切片都已经传输给后端
- 文件合并:发送切片完成后,发送一个合并请求,后端收到请求,将之前上传的切片文件合并
如何实现localStorage定时清除
localStorage除非人为手动清除,否则会一直存放在浏览器中,可以使用storageTiming设置localStorage定时删除;也可以给其原型上添加set方法,在设置值时将当前时间以及有效时长记录进去,再重写一个get方法,获取值时先判断是否过期,如果过期就删除,并返回null,没过期正常返回
由于js是单线程,如果遇到大量计算的场景,如图像处理,视频转码等,js线程会被长时间阻塞,甚至造成页面卡顿,影响用户体验,为解决这一弊端HTML5提供的一种浏览器内置的多线程解决方案——WebWorker,通过JS API来创建一个独立于主线程并且可与其并行运行的工作线程。用来执行一些长时间运行的计算密集型任务,从而不会阻塞主线程的执行。
优点:
- 提高程序的响应速度:WebWorker将代码运行在自己分离出来的线程上,可以避免单线程模式下,避免单线程模式下大量计算造成的卡顿问题,提高了程序的响应速度
- 充分利用多核CPU:WebWorker可以比较简单的实现多线程并行处理,充分利用了多核CPU,提高程序的执行效率
- 更好的代码结构和模块化:通过将代码分为主线程和工作线程,使代码结构更加合理,同时可以更好的实现项目组织和模块化管理
使用限制:
- 同源限制:分配给worker线程的脚本,必须和主线程脚本同源(否则无法创建worker,且双方无法通信);
- DOM限制:Worker线程所在的全局对象与主线程不一样,无法读取主线程的DOM对象,也无法使用document、window、parent这些对象,但是Worker线程可以访问navigator对象和location对象
- 通信限制:由于Worker单独运行在一个子线程,不在同一个上下文环境,不能直接通信,所以和主线程通信使用发布、订阅的消息机制完成
- 脚本限制:Worker线程不能执行alert()方法和confirm()方法,但是可以在Worker中使用XMLHttpRequestd来发送异步请求
- 文件限制:Worker线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本必须来自网络。
jquery如何实现链式调用
let fun = {
fun1: function() {
console.log("fun1");
return this;
},
fun2: function() {
console.log("fun2");
return this;
},
fun3: function() {
console.log("fun3");
return this;
}
}
fun.fun1().fun2().fun3();
node事件循环机制和浏览器事件循环机制有什么区别
浏览器和node环境下,微任务队列的执行时机不同
- Node端,微任务在事件循环的各个阶段之间执行
- 浏览器端,微任务在事件循环的宏任务执行完之后执行
讲一讲Reflect
Reflect是ES6起js的内置对象,提供拦截js操作的方法,是ES6为了操作对象而提供的新API。Reflect不是一个函数对象,因此他是不可构造的;像Math对象一样,Reflect对象的所有属性和方法都是静态的。好处是:
- 现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署Reflect对象上。
- 修改某些Object方法的返回结果,让其变得更规范化。如Object.defineProperty(obj,name,desc)在无法定义属性时,会抛出一个错误,而Reflect.define.defineProperty(obj,name,desc)则会返回false
- 让Object操作都变成函数行为,例如Object的命令式操作name in obj和delete obj[name]与Reflect.has(obj,name)、Reflect.delecteProperty(obj.name)相等
in运算符:判断一个对象是否存在某个属性,delete运算符:删除对象属性 - Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,Reflect对象是就有对应的方法
Object.keys和Object.getOwnPropertyNames有什么区别
首先,他们的共同点:都是返回自身的属性,不会返回原型链上的;
区别:Object.keys()返回可枚举的属性,Object.getOwnPropertyNames()返回自身的所有的属性。(Symbol除外)
clientHeight、offsetHeight、scrollHeight有什么区别
- clientHeight:用户可见内部高度+padding
- offsetHeight:总高度,算上滚动条
- scrollHeight:可滚动高度+padding
- scrollTop:当前元素距离顶部的距离
触底加载:
页面被卷去的高度+可视的DOM高度(视口高度)>=滚动条的长度
scrollTop+clientHeight>=scrollHeight-50px
BOM和DOM的区别
BOM和DOM联系:BOM是包括DOM的,即window包括document
区别:
- BOM是浏览器对象模型,通过BOM的对象和方法可以完成对浏览器的窗口操作,如:关闭浏览器,前进、后退、修改地址栏上地址等。BOM的顶级内置对象是window
- DOM是文档对象模型,通过DOM对象和方法可以完成对网页中元素的增删改,让网页产生动态效果。DOM的顶级对象是document
setTimeout与setInterval区别
setTimeout和setInverval都属于JS中的定时器,可以规定延迟时间在执行某个操作
setTimeout()方法用于在指定的毫秒数后调用函数或计算表达式,而setInterval()则可以在每隔指定的毫秒数循环调用函数或表达式,直到clearInterval将其清除
fetch的优缺点
fetch API是基于promise的设计,为了取代xhr的不合理写法而生的
优势:语法简洁,更加语义化;基于标准Promise实现,支持async/await;更加底层,提供的API丰富;脱离XHR,是ES规范里新的实现方式
缺点:只对网络请求报错,对400、500都当作成功的请求;fetch默认不会带cookie,需要添加配置项;fetch不支持超时timeout处理;fetch无法原生检测请求进度,而xhr可以
秒传、分片传输、断点传输
- 秒传:文件上传前,服务器先对文件做MD5校验,如果服务器上有相同的文件,则返回一个新地址,如果不想秒传也可以,修改文件中的内容就可以了(改名字不行)
- 分片传输:利用Blob提供的slice方法把大文件分割为一个个小文件分别传输。全部上传完成时候由服务器进行归总整合
- 断点传输:在分片上传的基础上,分成一个个小文件之后,每个小文件上传完毕之后对其进行状态的存储(localStorage),如果中间发生网络断线或者刷新,下次可以接着上次的进度上传
JS性能优化的方式
垃圾回收、闭包中的对象清除、防抖节流、分批加载、懒加载、事件委托、requestAnimationFrame的使用、script标签中的defer和async、CDN(内容分发网络)
计算机网络
跨域
跨域的方式都有哪些?他们的特点是什么?
当一个请求url的协议、域名、和端口号三者任意一个与当前页面不同时即为跨域。会出现跨域问题,是因为浏览器的同源策略限制,它是浏览器最核心也是最基本的安全功能,如果缺少同源策略,浏览器很容易受到XSS、CSRF等攻击。
同源策略限制内容有:cookie、localStorage、indexedDB等存储性内容;DOM节点;AJAX请求后,结果被浏览器拦截了,但是有三个标签是允许跨域加载资源。所以说跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。
<img src="xxx">
<link href="xxx">
<script src="xxx">
跨域解决方式有:JSONP、CORS、postMessage、websocket、Node中间件代理(两次跨域)、nginx反向代理、window.name+iframe、location.hash+iframe、document.domain。
CORS支持所有类型的HTTP请求,是跨域HTTP的根本解决方案;JSONP只支持GET请求,JSONP的优势在于支持老式浏览器,可以向不支持CORS的网站请求数据。Node中间件代理与nginx反向代理的跨域原理是同源策略对服务器不加限制。
- JSONP
JSONP跨域其实是利用了标签中src属性不受跨域限制的特点。可以拿到外部资源。虽然src、script标签都有src属性,但是img只是拿资源回来,并没有进一步做任何js的动作;script标签,在拿到资源的同时,还会将资源内容作为js代码运行。
核心点就是利用script标签,可以跨域请求云端的资源,并且把云端的资源请求到本地。把资源作为脚本运行。
优点:兼容性好,可用于解决主流浏览器的跨域数据访问的问题;缺点:仅支持get方法,不安全可能会受到XSS攻击
- CORS
通过自定义请求头来让服务端和浏览器进行沟通。
这种方式解决跨域问题,会在发送请求时出现两种情况,分为简单请求和复杂请求
简单请求:
- 请求方式GET HEAD POST ;
- 请求头不超过以下几种字段:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-type的值只限于三个值为:text/plain、multipart/form-data、application/x-www-form-urlencoded;
- 简单请求,浏览器自动添加一个Orgin字段;
- 同时后端需要设置的请求头:Access-Control-Allow-Origin【必须】;Access-control-Expose-Headers【XMLHttpRequest只能拿到六个字段,要想拿到其他的需要在这里指定】;Access-Control-Allow-Credentials【是否可传cookie】,要是想传cookie,前端需要设置xhr.withCredentials=true,后端设置Access-Control-Allow-Credentials
非简单请求
- 浏览器先发送一个header头为option的请求进行预检;预检请求格式(请求行的请求方法为OPTIONS【专门用来询问的】);Origin、Access-Control-Request-Method、Access-Control-Request-Header
- 浏览器检查了Origin、Access-Control-Request-Method和Access-Control-Request-Header之后确认允许就可以做出回应了
- 通过预检后,浏览器接下来每次请求都类似于简单请求了
- postMessage
postMessage是H5 引入的API,可以更方便、有效、安全的解决这些问题。postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文跨文本档、多窗口、跨域消息传递
a窗口向b窗口发送数据,先把data转为json格式在发送。提前设置好message监听;b窗口进行message监听,监听到了同样的方式返回数据;a窗口监听到了message,进行一系列操作
- websocket
websocket是H5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案,websocket和HTTP都是应用层协议,都基于TCP协议,但是websocket是一种双向通信协议,在建立连接之后websocket与client都能主动向对方发送或接收数据。同时,websocket在建立连接时需要借助HTTP协议,连接建立好之后client与server之间的双向通信就与HTTP无关了
- Node中间件代理(两次跨域)
实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略
代理服务器
接受客服端请求--将请求转发给服务器--拿到服务器响应数据--将响应转发给客户端
- nginx反向代理
原理类似于Node中间件代理,需要你搭建一个中转的nginx服务器,用于转发请求
使用nginx反向代理实现跨域,是最简单的跨域方法。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。
总结:CORS支持所有类型的HTTP请求,是跨域HTTP的根本解决方案;JSONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据;不管是node中间件代理还是nginx反向代理,主要是通过同源策略对服务器不加限制;日常工作中用的较多的跨域方案是CORS和nginx反向代理
- window.name+iframe
window.name属性的特点是name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的name值(2MB)。
通过iframe的src属性由外域转向本地域,跨域数据既由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作
- location.hash+iframe
因为hash传值只能单向传输,所有可以通过一个中间网页,a若想跟b进行通信,可以通过一个与a同源的c作为中间网页,a传给b,b传给c,c在传回a,三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信
具体做法:在a中放一个回调函数,方便c回调,放一个iframe标签,随后传值
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe> <script> var iframe = document.getElementById('iframe'); // 向b.html传hash值 setTimeout(function() { iframe.src = iframe.src + '#user=admin'; }, 1000); // 开放给同域c.html的回调方法 function onCallback(res) { alert('data from c.html ---> ' + res); } </script>
在b中监听哈希值改变,一旦改变,把a要接收的值传给c
<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe> <script> var iframe = document.getElementById('iframe'); // 监听a.html传来的hash值,再传给c.html window.onhashchange = function () { iframe.src = iframe.src + location.hash; }; </script>
在c中监听哈希值改变,一旦改变,调用a中的回调函数
<script> // 监听b.html传来的hash值 window.onhashchange = function () { // 再通过操作同域a.html的js回调,将结果传回 window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', '')); }; </script>
- document.domain
只能跨子域,需要主域相同才能使用
实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域;
适用于嵌入iframe页面的情况,iframe优点是内联框架,可以将页面嵌入别的页面,提高代码的可重用;缺点是会阻塞主页面的onload事件;搜索引擎爬虫还不能很好的处理iframe中的内容,不利于搜索引擎优化(SEO)
网络原理
讲一讲三次握手
三次握手的作用是客户端与服务端建立连接。
第一次握手 客户端发起连接请求,发送SYN包到服务器,并指明客户端的初始化序列号
第二次握手 服务端收到SYN包,会发送自身的SYN包作为响应,并指明自己的初始化序列号,同时会把客户端的序列号+1作为ACK的值,表明服务端收到了客户端的SYN,向客户端发送ACK确认包
第三次握手 客户端收到了服务器的SYN-ACK包,将自身序列号+1、同时将服务器序列号+1作为ACK的值,表明客户端收到了服务器的SYN,向服务器发送ACK确认包,此时三次握手完成,客户端和服务器建立TCP连接
为什么需要三次握手呢
首先三次握手的目的就是为了确认客户端与服务端双方接收和发送能力是否正常。
第一次握手是客户端发送,服务端接收,第二次是服务端发送,客户端接收;
两次握手完毕,客户端可以确认服务端的接收和发送能力正常,而服务端并不能确认客户端接收能力是否正常,所以第三次客户端发送,服务器接收。至此,双方都能确认对方的接收和发送能力正常
四次挥手
四次挥手的作用是断开连接:
第一次挥手 客户端主动断开连接,发送FIN包给服务器,并指定一个序列号,此时客户端还可以接收数据
第二次挥手 服务器收到FIN包 将客户端的序列号+1作为ACK包的序列号给客户端,表明收到客户端的请求,同时服务器会将缓存中还没有发送完的1数据继续发送给客户端
第三次挥手 服务器发送完数据,会给客户端发送FIN数据包并指定一个序列号以及ACK确认包,用来停止向客户端发送数据
第四次挥手 客户端收到FIN之后,会发送ACK确认包给服务器,将服务器的序列号+1作为ACK包的序列号,之后客户端等待一段时间后,进入关闭状态
为什么客户端要等待一段时间关闭
一方面是为了确保服务器收到最后一个ACK包,确保正确的结束这次连接。在这段时间里,如果最后一个ACK包没有到达服务器,服务器会重新发送第三次挥手的报文给客户端,客户端收到之后,就知道第四次挥手的ACK包丢失了,之后再次发送ACK包;
另一方面是保证这次连接期间内所产生的所有请求报文生效。防止在下一个新连接中出现旧连接的请求报文
为什么连接的时候是三次握手,关闭的时候却是四次挥手
首先建立连接时,服务器在收到客户端的SYN包时,可以直接发送ACK包来应答,SYN包来同步。
但是在关闭连接时,服务端收到了FIN包时,此时可能还有未传输完的数据,服务端不会马上关闭,需要先回复FIN包,告诉客户端,已经收到请求,之后等数据传输完,在发送FIN包,所以是四次挥手
有没有可能三次挥手
可以通过TCP协议中的捎带应答,第二次挥手服务端返回客户端ACK的时候,可以在延迟一段时间内将服务端给客户端的FIN响应一并返回给客户端,这样可以进一步提升效率,四次挥手在某些情况下也可能会变成三次挥手
序列号ISN是固定的吗
序列号是动态随机生成的,每个连接都有不同的序列号。
首先,从安全性考虑,固定的序列号很容易被攻击者猜出后续的序列号,从而遭受到攻击
其次,从连接稳定性来看:如果连接状况不好,不停的断开重连,此时,固定的序列号可能会出现新连接中收到旧连接请求报文,从而出现数据乱序的问题
三次握手过程中可以携带参数吗
在三次握手中,第一次握手是不允许携带参数的,但第三次握手可以携带参数
首先,肯定是基于安全性考虑,第一次握手就携带参数的话,会大大增加服务器遭受攻击的风险;
其次,从性能方面考虑,第一次握手携带大量数据,服务器会花费较长时间进行解析,如果重复发送请求,服务器还可能会发生崩溃
而在第三次握手时客户端已经建立好连接,同时客户端明确服务器和自身接收发送能力都正常,所以此时可以携带数据
传输过程中握手报文丢失怎么办
如果是第一次握手报文丢失:客户端长时间接收不到服务器的SYN-ACK报文,会触发客户端的超时重传机制,客户端重传SYN报文序列号相同
需要注意的是,超时时间一般是写死在内核中,一般默认重传5次,每次超时时间是上次的2倍,当第5次重传后,会继续等待32秒。如果仍然没有回应,此时将断开TCP连接,总耗时大约1分钟左右
如果是第二次握手报文丢失:客户端收不到SYN-ACK会认为自己的SYN丢失,服务端收不到ACK,会任务自己的SYN-ACK丢失,此时客户端与服务端都会触发超时重传机制进行重传
如果是第三次握手报文丢失:此时服务端收不到ACK,会认为自己的SYN-ACK丢失,触发超时重传,直到收到客户端的ACK报文,或者达到最大重传机制,此时客户端会等待一段时间关闭,并不会重传ACK,如果ACK丢失了,客户端会收到服务器重传的SYN-ACK,此时客户端重新发送ACK
什么情况下报文会失效或丢弃
一是:服务器的半连接队列满了,客户端就会一直超时重传,直到到达最大重传次数
二是:服务器的连接队列满了
关于SYN-ACK重传次数的问题
服务器发送完SYN-ACK包,如果未收到客户端确认包,服务器进行首次重传,等待一段时间后仍未收到客户端确认包,进行第二次重传。如果重传次数超过系统规定的最大重传数,系统将该连接从半连接队列删除。注意,每次重传等待时间不一定相同,一般是指数增长
什么是半连接队列
服务器第一次接收客户端的SYN包,此时双方还没有完全建立连接,服务器会把这种状态下的请求连接放到一个队列里,这种队列称之为半连接队列
当然还有全连接队列,就是已经完成三次握手建立起连接的会放在全连接队列中,如果队列满了就有可能出现丢包现象
SYN泛洪攻击
SYN泛洪攻击属于DOS攻击的一种,利用TCP协议三次握手机制,通过大量伪造IP向服务器发出SYN连接请求,可以在服务器中创建大量的TCP半连接,为了维护庞大的半连接列表,服务器会消耗非常多资源。忙于处理攻击端伪造的TCP连接请求而无法处理正常连接请求,甚至会导致堆栈的溢出崩溃。
办法:缩短对半连接握手状态的等待时间,增加最大半连接 采用SYN cookies技术
XSS攻击
跨站脚本攻击,利用网页开发时留下的漏洞,恶意攻击者往Web页面里插入恶意的script代码,当用户浏览时,嵌入其中Web里面的script代码就会被执行,从而达到恶意攻击用户的目的
主要危害有:
- 通过document.cookie盗取cookie中的信息
- 使用js或者css破坏页面正常结构
- 流量劫持(通过访问某段具有window.location.href定位到其他页面)
- dos攻击:利用合理的客户端请求来占用过多的服务器资源,从而使合法用户无法得到服务器响应。并且通过携带过程的cookie信息可以使服务器端返回400开头的状态码,从而拒绝合理的请求服务
- 利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻击)用户的身份执行一些管理动作,或者执行一些一般如发微博、加好友、发私信等操作、并且攻击者还可以利用iframe、frame进一步进行CSPF攻击。
- 控制企业数据,包括读取、篡改、添加、删除企业敏感数据的能力
XSS攻击分为:反射型XSS攻击与存储型XSS攻击
存储型XSS攻击:是攻击者先将恶意代码上传或存储到漏洞服务器中,只要受害者浏览包含此恶意代码的页面,就会执行恶意代码,这意味着只要访问了这个页面的访客,都有可能执行这段恶意脚本,因此存储型XSS的危害会更大。存储型XSS一般出现在网站留言、评论、博客日志等交互处,恶意脚本存储到客服端或者服务端的数据库中,存储型XSS攻击更多的时候用于攻击用户,而且在工作中的防范更多的是防范存储型XSS攻击
反射型XSS攻击:一般是攻击者通过特定手法(如电子邮箱),诱使用户去访问一个包含恶意代码的URL,当受害者点击这些设计的链接的时候,恶意代码会直接在受害者主机上的浏览器执行。
对于访问者而言是一次性的,具体表现在我们的恶意脚本通过URL的方式传递给了服务器,而服务器则只是不加处理的把脚本“反射”回访问者的浏览器而使访问者的浏览器执行相应的脚本。要避免反射型XSS,必须要后端协调,后端解析前端数据时首先做相关字串检测和转义处理,此类XSS通常出现在网站搜索栏、用户登录等地方,常用来窃取客户端Cookies或进行钓鱼欺骗
什么是CSRF
- CSRF跨域请求伪造
在未退出A网站的前提下访问B,B使用A的cookie去访问服务器
防御:
1. 使用验证码或者 token 验证,每次提交表单时需要带上 token(伪造者访问不到),如果 token 不合法,服务器拒绝请求
2. 通过 host+origin 来判断是否为非法用户
3. 给 Cookie 设置 SameSite属性,来限制第三方 Cookie,里面有三个值 strict、lax、none
strict:最严格,完全禁止第三方的 cookie;但是体验不好,如果当前有一个 github 链接,点击跳转就不会携带任何 cookie,跳转过去一直是未登录状态的
lax:稍微放宽了一些,大多不发送 cookie,但除了 get 请求(只包括三种情况:链接、预加载请求、get 表单)以外
none:关闭该设置
HTTP的结构
请求行 请求头 空行 请求体
请求行包括http版本号 url 请求方式
响应行包括版本号 状态码 原因
HTTP头都有哪些字段
- 请求头
cache-control是否使用缓存
connection:keep-alive与服务器的连接状态
Host主机域 - 返回头
cache-control
etag唯一标识,缓存用的
last-modified最后修改时间
HTTPS与HTTP的一些区别
- 安全性不同:HTTP是一个简单的请求-响应协议,特点是无状态和明文传输,而HTTPS,实际上是HTTP加上SSL协议组成的一种加密传输协议,保护用户的个人隐私和一些敏感数据。
- 响应速度:理论上HTTP响应速度快,因为HTTP只需要三次握手就能建立连接,而HTTPS除了三次握手,还需要进行SSL握手,一共需要12个包
- 端口号不同:HTTP和HTTPS使用的是完全不同的连接方式,前者是80端口,后者是443端口
- 消耗资源:HTTPS是构建在SSL之上的HTTP协议,会消耗更多的服务器资源
- 展示方式:HTTP站点,会被谷歌浏览器标记为“不安全”等等,HTTPS站点,则会被各大浏览器加上“绿色安全锁”标记,如果2网站配置增强级SSL证书,地址栏还会变为“绿色地址栏”
- 费用不同:HTTPS需要为网站购买和配置SSL证书,会产生一定费用
HTTP1.0、HTTP1.1、HTTP2.0
- HTTP0.9只支持get请求
- HTTP1.0增加了POST、HEAD、OPTION、PUT、DELETE等
HEAD请求:和get请求差不多,但是没有body,用来检查资源是否有效,不需要消耗更多的带宽去请求这个url
OPTION请求:预检请求,判断是否支持跨域(CORS)
PUT一般是用来更高资源,post是增加资源 - HTTP1.1比HTTP1.0性能上改进:
HTTP1.1使用长连接改善短连接造成的性能开销
支持网络传输,只要第一个请求发送,不必等其回来,就可以发送第二个请求,减少整体响应时间 - HTTP1.1性能瓶颈:
请求/响应头未经压缩就发送,首部信息越多,延迟越大,只能压缩body部分
每次互相发送相同的首部造成的浪费较多
如果服务器响应慢,会导致客户端一直请求不到数据,也就是队头阻塞
没有请求优先级控制
请求只能从客户端开始,服务器被动响应
- HTTP2.0优化:
头部压缩 如果同时发送多个相同请求头的请求,协议会自动消除重复部分,提高了速度
二进制格式 增加了数据的传输效率
数据流 同一个连接里面连续的数据包可能属于不同的回应,都标记这唯一的编号,服务器发出的数据包为偶数,客户端发送的数据包为奇数 客户端可以指定数据流的优先级,优先级高,先响应
多路复用 一个连接中并发多个请求或回应 不用按照顺序对应 降低延迟 提高连接利用率
服务器推送 服务器可以主动向客户端发消息 - HTTP2.0缺陷:
多个请求复用一个TCP连接,一旦丢包,就会阻塞所有的HTTP请求
- HTTP3.0优化
因为TCP是传输层的问题,所以HTTP3.0将HTTP下层协议改成了UDP
UDP发送时是不管顺序,也不管丢包的,所以不会出现HTTP1.1的队头阻塞和HTTP2.0的一个丢包全部重传的问题
UDP是不可靠传输,但是基于UDP的QUIC协议可以实现类似TCP的可靠性传输
说说你知道的状态码
- 2开头的表示成功:一般见到的是200
- 3开头的表示重定向:301永久重定向、302临时重定向、304表示可以在缓存中去数据(协商缓存)
- 4开头表示客户端错误:403跨域、404请求资源不存在
- 5开头表示服务端错误:500
网络OSI七层模型都有哪些?TCP是哪一层的
七层模型:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层
TCP属于传输层
localStorage、SessionStorage、cookie、session之间有什么区别
- webStorage API:是浏览器端数据存储机制,只能存储字符串,以键值对保存可以将对象JSON.stringify()编码后存储
localStorage:
生命周期:关闭浏览器后数据依然保留,除非手动清除,否则一直在
作用域:相同浏览器的不同标签在同源情况下可以共享localStorage
sessionStorage:
生命周期:关闭浏览器或者标签后即失效
作用域:只在当前标签可用,当前标签的iframe中且同源可以共享
localStorage与sessionStorage区别:
localStorage用于长久的保存整个网站数据,保存的数据没有过期时间,只能手动去删除
sessionStorage用于临时保存同一窗口下的数据,再关闭窗口之后将删除数据
方法:
- xxxStorage.setItem('msg','维生素') :设置值
- xxxStorage.getItem('msg'):获取
- xxxStorage.removeItem('msg'):删除
- xxxStorage.clear():删除所有值
- xxxStorage.key():获取键值
- cookie
一开始cookie不是用来存储的,而是为了弥补http的状态的不足,http是无状态协议。每当向服务器发起请求、请求结束,下次发送请求的时候服务端就不知道是谁了,所以cookie是用来弥补这个不足的
cookie有很多缺陷,比如:
容量缺陷。cookie的存储空间只有4KB
性能缺陷。有时候cookie我们用不到,但是不管用得到用不到,http在发送请求的时候一定会带着cookie,这就造成了性能的浪费
安全缺陷:cookie在http下很容易被非法用户获取。尤其是设置了http-only为false的情况下,这个时候js可以读取到cookie,很容易受到XSS攻击
cookie是保存在客户端的,一般由server设置值及过期时间
cookie没有提供删除API,如果想要删除的话可以把max-age设为0或者把expire设置为当前时间(立刻过期)即可
cookie的属性由
- http-only:不能被客户端更改访问,防止XSS攻击
- max-age:生效后存活的时间
- Secure:是否只允许在http下传输
- expire:过期时间
- session:
session是保存在服务端的
session的运行依赖sessionId,sessionId又保存在cookie中,所以禁用了cookie之后session也用不了了,除非将sessionId存储在url中
session一般是用来跟踪用户状态的
session比较安全,因为存储在服务器中,不过为了减少服务端的压力,很多信息还是推荐存在cookie中
indexDB与localStorage的区别
都是在客户端存储数据,可以存储较多数据,且都是永久性保存,都通过键值对存储数据
不同:
- indexDB是JS脚本语言可以操作的数据库
- 操作说异步的
- indexDB支持事务,一系列操作步骤中,只要有一步失败,整个事务都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况
- 存储空间大indexDB的存储空间比localStorage大得多,一般来说不少于250MB.甚至没有上限
- localStorage不支持搜索
服务端渲染和客户端渲染的区别,各自的优缺点
- 服务端渲染(SSR Server Site Rendering)
- 有利于 SEO,首屏加载快,但是重复请求次数多,开发效率低,服务器压力大
- 渲染的时候返回的是完整的 html 格式
- 应用场景:可能被搜索到的
- 客户端渲染(CSR Client Site Rendering)
- 不利于 SEO,首屏加载慢,前后端分离开发,交互速度快、体验好
- 渲染的时候返回的是 json 数据格式,由浏览器完成渲染
- 应用场景:app 内部"嵌套"的 h5页面
什么是JWT(JSON Web Token)
在没有 JWT 之前,验证客户端的方式就是通过 token,具体方式如下
用户输入账号密码以后,向服务端发起请求,服务端生成token返回给客户端,然后下次客户端请求数据的时候会携带着 token,服务端收到之后,会与之前保存的 token进行验证,验证通过以后返回数据,验证不通过就不返回。
不过这种方式的扩展性很差,因为如果有上万个用户发起请求的话就需要保存上万条 token,这样对服务端而言无疑是压力巨大的
后来出现了 JWT,这种方法可以把 token 保存在客户端
JWT 相当于把数据转换成 JSON 对象,这个特殊的 JSON 对象分为三部分:头部、负载、签名,他们之间分别用 . 区分开
- 头部(header)
- 保存的是JWT 的元数据,表明所使用的 hash 算法,以 JSON 对象的方式存储,然后转换成 Base64URL 的格式
- 负载(payload)
- 也是 JSON 对象格式,用来存放自己的数据
- 签名(Signature)
- 确保消息的完整性
get和post的区别
首先,get请求与post请求是两种常用的请求方式,本质上都是基于TCP/IP协议实现的,使用二者中的任意一个,都可以实现客户端和服务端的双向交互。
区别在于:
传参方式不同:get是通过url传递参数,post是把数据放在request body中
不同的传参方式导致的长度限制不同:get有url长度限制,而post没有长度限制
安全性问题:get通过url传递的数据会显示在用户面前,肯定不安全,而post不会作为url的一部分,也不会被缓存,但能被保存在服务器日志以及浏览器浏览记录中
浏览器回退导致的结果不同:get请求可以直接进行回退和刷新,不会对用户和程序产生任何影响;而post请求如果直接回退和刷新将会再次提交数据请求
get请求比post请求速度更快:因为get和post请求本质都是tcp链接 导致不同的是http协议,post产生两个tcp数据包,get只产生一个数据包,网络好的时候差别不大,网络差的时候,两次tcp在验证数据包完整性上比较有优势
静态数据缓存方面:如果get请求的是静态资源,会在第一次打开时进行缓存处理,这样用户第二次访问相同地址时能快速打开,而post则不行
post不能进行管道化传输
所谓管道化传输,就是将多个HTTP要求整批提交的技术,而在传送过程中不需先等待服务端的响应。
因此一般来说,post用于修改和写入数据,而get一般用于搜索排序和筛选之类的操作,目的是获取资源,读取数据
讲一讲http缓存
- 缓存分为强缓存和协商缓存
- 强缓存
在客户端发起请求之前,先检查强缓存,查看强缓存的cache-control里的max-age,判断数据有没有过期,如果没有直接使用该缓存 ,有些用户可能会在没有过期的时候就点了刷新按钮,这个时候浏览器就回去请求服务端,要想避免这样做,可以在cache-control里面加一个immutable.这样用户再怎么刷新,只要 max-age 没有过期就不会去请求资源。
- 协商缓存
浏览器加载资源时,没有命中强缓存,这时候就去请求服务器,去请求服务器的时候,会带着两个参数,一个是If-None-Match,也就是响应头中的etag属性,每个文件对应一个etag;另一个参数是If-Modified-Since,也就是响应头中的Last-Modified属性,带着这两个参数去检验缓存是否真的过期,如果没有过期,则服务器会给浏览器返回一个304状态码,表示缓存没有过期,可以使用旧缓存。
etag的作用
有时候编辑了文件,但是没有修改,但是last-modified属性的时间就会改变,导致服务器会重新发送资源,但是etag的出现就完美的避免了这个问题,他是文件的唯一标识
last-modified和etag各有各自的优点和缺点:
每个文件都有一个 etag 和 last-modified 属性,etag 要优先于 last-modified,两个属性各有优缺点,比如 last-modified 很多时候不能感知文件是否改变,但 etag 能;last-modified 仅仅记录了时间点,性能肯定要高于etag,etag 记录的是哈希值
缓存位置:
- 内存缓存Memory-Cache
- 离线缓存Service-Worker
- 磁盘缓存Disk-Cache
- 推送缓存Push-Cache
TCP和UDP有什么区别
TCP:传输控制协议,是一种面向连接、可靠的、基于字节流的传输层通信协议
UDP:是用户数据报文协议,是一个简单的面向数据报的传输层协议
区别:
- TCP是面向连接的,UDP是面向无连接的
- 在发送端,应用层将数据传递给传输层的UDP协议,UDP只会给数据增加一个UDP头,标识是UDP协议,然后就传递给网络层了。
- 在接收端,网络层将数据传递给传输层,UDP只去除IP报文头就传递给应用层了,不做任何拼接操作。
- TCP是可靠的,使用流量控制和拥塞控制,UDP是不可靠的,不使用流量控制和拥塞控制
- 具体体现在,UDP的无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。
- 发送数据也不会关心对方是否已经正确接收到数据。
- 其次,网络环境时好时坏,但是UDP是没有拥塞控制的,一直会以恒定的速度发送数据。就算网络不好,也不会对发送速率作调整,这样就会在网络不好时,可能产生丢包。但是优点也明显,就是对于一些实时性要求高的场景(比如电话会议)就需要UDP,因为远程视频的话,你丢一些数据(例如像素)并不影响视频的内容。
- 在TCP协议中使用了接收确认和重传机制,使得每一个信息都能保证到达,是可靠的。UDP是尽力传送,没有应答和重传机制,UDP只是将信息发送出去,对方收不收到也不进行应答。所以UDP协议是不可靠的
- TCP是面向字节流的,UDP是面向报文的
- TCP基于流的传输表示TCP不认为消息是一条一条的,是无保护消息边界的(保护消息边界:指传输协议把数据当做一条独立的消息在网上传输,接收端一次只能接受一条独立的消息)。
- UDP面向报文,是有 保护消息边界 的,接收方一次只能接受一条独立的消息,所以UDP不存在粘包。
- TCP只有一对一传输方式,而UDP不仅可以一对一,还可以一对多,多对多
- UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。
- TCP不能一对多的原因是:TCP通信前要跟一台主机进行三次握手连接,因此TCP不能对多
- UDP的头部开销小,TCP的头部开销大
UDP的头部很小,只有8个字节。相比TCP至少20个字节,UDP头部要小得多。
- TCP会产生粘包问题,UDP会产生丢包问题
- TCP产生粘包问题的主要原因是:TCP是面向连接的,所以在TCP看来,并没有把消息看成一条条的,而是全部消息在TCP眼里都是字节流,因此A、B消息混在一起后,TCP就分不清了。
- 由于UDP是没有应答和重传机制,因此包很容易传丢了也不知道。
适用场景:UDP适用于实时应用(IP电话、视频会议、直播等)、TCP适用于要求可靠传输的应用,例如文件传输
TCP协议
首先TCP是对数据传输提供的一个管控机制,保证数据可靠传输的情况下尽可能提高效率。
TCP协议中通过两个核心机制,确认应答和超时重传,来保证可靠传输
确认应答机制:发送方发送一个数据包后,会等待接收方返回一个确认应答的数据包,再进行数据传输。
- 实现方式:通过序列号和确认序列号保证消息传输的可靠性,防止丢包现象产生。
- 序列号:位于TCP报文首部里面一个字段“32位序号”,发送方在首部里添加序号,接收方根据首部序号接收,之后接收方发送确认序列号给发送方,将收到的序列号+1,表示之前的数据已经收到
超时重传机制:发送的数据包可能会因为网络等原因,在一定时间内,没有收到确认应答,需要重新发送,解决丢包问题,实现可靠传输
连接管理机制:在发送数据前先通过三次握手建立连接,数据发送完成后通过四次挥手断开连接
流量控制机制:TCP协议根据接收端接收数据能力,来决定发送端发送数据速度的机制。如果发送端发送数据太快,可能会导致接收缓冲区被填满,继续发送数据的话,可能造成丢包,导致一系列连锁问题
过程:接收端将自己剩余缓冲区大小存入TCP头部中的“16位窗口大小”字段,通过ACK通知发送端;窗口大小越大,说明网络吞吐量越高;发送端根据接收这个窗口的大小,控制自己的发送速度;如果接收缓冲区满了,窗口会设置为0,发送端不再发送数据,而是定期发送一个窗口的探测报文。
拥塞控制:TCP通过慢启动的方式,先发少量数据探路,再决定发送速度。此时引入拥塞窗口,刚开始时,拥塞窗口设置为1,每收到一个ACK,拥塞窗口+1,每次发送数据,拥塞窗口和流量窗口的较小值作为实际发送窗口大小
TCP协议中应答机制保证了传输可靠性,缺点是效率太差,为了提高效率采用滑动窗口、延时应答、捎带应答
滑动窗口:本质上是批量传输数据,相当于把多份数据的传输时间和等待响应的时间压缩成一份了,总的等待时间少了,传输效率也就高了。操作系统内核为维护这个滑动窗口,需要开辟缓冲区来记录当前还有哪些数据没有应答,只有应答过的数据才能从缓冲区删掉
延时应答:目的是为了提高效率,在流量控制的基础上,不影响可靠性的前提下,让ACK的发送时间晚一会,延迟的时间中就会给应用程序提供更多的消费数据的机会,延时时间到了,再发送ACK,注意,延时应答的等待时间不能超过超时重传的时间
捎带应答:在延时应答的基础上,为了进一步提高程序允许效率,服务端返回给客户端ACK的时候,可以在延迟一段时间,将服务端给客户端的响应一并返回给客户端,这样就提升了效率。
TCP是如何保证可靠传输的
- 校验和
数据传输过程中,每一个数据段都有一个16位的编号,将这些编号加起来并取反得出一个校验和,看收到后是否和之前的一致 - 序列号和确认应答
每次发送数据的时候,服务端都会返回一个确认应答以及将要发送的序列号 - 超时重传、滑动窗口、拥塞控制
为什么TCP要进行流量控制
为了解决发送方和接收方的速率不一致的问题,如果发送方的速率过快的话,接收方处理不过来,只能放在缓存区,缓存区满了,就只能丢包了。所以需要进行流量控制
TCP为什么会重传
TCP 传输是一应一答的,如果中间丢包了的话,那么就会处于僵持状态,所以在发送方会设置一个定时器,一段时间(这个时间应该略大于一个发送来回的时间)如果没有收到对方ACK确认的话,就会重新发送数据,这就是超时重传
如果要发送12345中间丢包的话 只收到了1、3、4、5,服务器检测出来,会连续发送三个Ack=2,触发快速重传,在定时器之前就完成重传
TCP的四种拥塞控制算法:
慢开始、拥塞控制、快重传、快恢复
TCP粘包问题
是指发送方发送的若干包数据到达接收方时粘成了一包,导致数据包不能完整的体现发送的数据。
出现粘包原因:由于TCP是面向字节流的传输协议,传输消息是没有边界的,因此在流传输中会出现粘包问题,分为两种情况
发送端:由于TCP协议本身的机制,客户端与服务器会维持一个连接,数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用Nagle算法(可配置是否启用)对较小的数据包进行合并(基于此,TCP的网络延迟要UDP的高些)然后再发送(超时或者包大小足够)。这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,而且数据包的合并在TCP协议中是没有分界线的,所以这就会导致接收方不能还原其本来的数据包。
接收方:TCP是基于“流”的。网络传输数据的速度可能会快过接收方处理数据的速度,这时候就会导致,接收方在读取缓冲区时,缓冲区存在多个数据包。在TCP协议中接收方是一次读取缓冲区中的所有内容,所以不能反映原本的数据信息。(放数据的速度 > 应用层拿数据速度)
解决办法:
对于发送方引起的粘包现象,可通过编程设置来避免。TCP提供了强制数据立即传送的操作指令push,TCP收到该操作指令后,立即将本段数据发送出去,不必等发送缓冲区满
对于由于接收方引起的粘包,可通过优化程序设计,精简接收进程的工作量,提供接收进程优先级等措施,及时接收数据,避免粘包现象
TCP的四元组是什么
-
四元组:
- 源 IP地址,目标 ip 地址,源端口,目标端口
-
五元组:
- 源 IP 地址,目标 IP 地址,协议号,源端口,目标端口
-
七元组:
- 源 IP 地址,目标 IP 地址,协议号,源端口,目标端口,服务类型以及接口索引
从浏览器输入url后都经历了什么
- 首先是先判断用户输入的是URL还是搜索内容,如果是搜索内容,地址栏会使用浏览器默认的搜索引擎,来合成带搜索关键字的URL,如果是URL,整合URL+对应协议头(http/https)形成完整的URL。浏览器接收到输入的URL后,先对URL解析,把我们请求需要的协议、域名、端口号、路径这些信息解析提取出来并构造一个HTTP请求
- 接着浏览器会先查找缓存资源,依次查找浏览器缓存-系统缓存(本地host文件)-路由缓存中是否有该地址页面,如果有则显示页面内容,如果没有则进行下一步
- 浏览器向DNS服务器发起请求,解析该URL中域名对应的IP地址,DNS服务器是基于UDP的,因此会用到UDP协议
- 接下来是跟服务器通过三次握手建立TCP连接,发送HTTP请求,服务器响应请求返回数据,关闭TCP连接
- 最后浏览器收到数据之后进行解析,HTML通过HTML解析器输出DOM树、CSS通过CSS解析器输出CSS规则、结合DOM树和CSS规则,计算DOM树每个节点具体样式,生成渲染树、浏览器根据渲染树开始布局和绘制,构建图层树,显示页面
说一下回流和重绘
回流:浏览器初始渲染阶段进行会触发回流,之后如果进行一些元素的添加删除,宽度高度等引起页面布局改变的操作,会重新渲染DOM。
重绘:元素属性改变不影响浏览器布局,比如对背景色,字体颜色修改,浏览器只对改变样式的元素进行重新绘制
触发回流:当元素的宽高内外边距,上下左右等尺寸或位置发生变化的时候会发生回流;内容发生改变时,比如文本变化,图片被未知大小图片覆盖的时候,页面初次渲染的时候浏览器窗口尺寸改变时;读写offset、client、scroll时,浏览器为了获取这些值,会进行回流操作;使用window.getComputedStyle的时候
触发重绘:回流一定触发重绘,颜色修改、文字方向修改、阴影修改等
减少回流重绘:修改元素样式时,尽量通过改变元素class类名进行样式替换;尽量避免添加过多的内联样式;制作动画时尽量使用css3中的transform去改变位置,不会触发回流;
讲讲CDN缓存
如果接入了CDN的话,DNS解析权也是会改变的,会把DNS解析权交给CNAME指向的CDN专用DNS服务器
其次就是缓存问题,正常情况下,浏览器在发起请求之前,会先查看本地缓存,如果过期的话再去向服务端发起请求;
如果使用了CDN,浏览器会先查看本地缓存,如果未命中,会向CDN边缘节点发起请求,如果没有过期,直接返回数据,此时完成http请求,如果过期,则CDN还需要向源站发起回源请求,来拉取最新数据。
CDN的缓存策略是因服务商的不同而不同的,但是大多会遵循http标准协议,通过Cache-Control里面的max-age来控制缓存时间
- CDN优点:
减少了用户访问延时,也减少了源站负载 - CDN缺点:
当网站数据更新时,而此时缓存数据还没有过期,这时候用户只需强制刷新即可获取最新数据。但是如果使用了CDN的话,用户强制刷新也只是请求到CDN上的数据,此时如果CDN没有同步最新数据的话会导致用户访问异常。一般这个时候程序员需要手动更新调用CDN的刷新缓存接口。
说一说defer和async
首先,在正常情况下,script 标签是会阻塞 DOM 的解析的,所以我们要尽量不要把script 放到 head 里,要尽量放到 body 的最下方。这样不至于会有白屏的效果;
然后是 defer和 async;他们两个是异步加载 js 代码的。
- defer
给 script 标签添加 defer 属性,就不会阻塞 dom 的解析了,等 dom 渲染完之后才会运行该 js 代码,如果是多个 script 标签都添加了 defer 属性的话,那么它们是按照顺序执行的(第一个全部执行完毕之后才能执行第二个),defer 的 script 是在 DOMContentLoaded 之前运行的。
- async
给 script 添加 async 属性之后,会异步下载 js 代码,等下载完成之后会立即运行js 代码。多个 script 标签同时设置了 async 是没有先后顺序的,谁先加载完谁就先运行。
如果 script 标签没有操作任何 dom 信息,且不彼此依赖的话,可以使用 async
meta标签可以做什么
为浏览器提供html的元信息
规定 html 字符编码
<meta charset="UTF-8">
设置移动端的视区窗口
<meta id="viewport" name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1; user-scalable=no;">
移动端点击300ms 延时,可以对放大禁用
<meta name="viewport" content="user-scalable=no">
<meta name="viewport" content="initial-scale=1,maximum-scale=1">
设置 http 头
<meta http-equiv="content-Type" content="text/html; charset=gb2312">
图片403
<meta name="referrer" content="no-referrer" />
dns 预解析
<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="//www.zhix.net">
什么是DNS预解析
- 正常输入 url 地址之后,会进行 dns域名解析,这个过程大概会花费20~120ms,所以这个时候出现了预解析,可以给当前页使用到的其他域名进行预处理,提前进行预解析,这样当再次访问这些域名的时候就不用 dns 解析了;
- 缺点:有时不会访问的网页页进行了预解析,有时为了节约性能,会选择关闭 dns 预解析。
- 使用方式:
<meta http-equiv="x-dns-prefetch-control" content="on"> // chrome 火狐新版等浏览器自动为 on,要关闭可以设置为 off
<link rel="dns-prefetch" href="//www.zhix.net">
<link rel="dns-prefetch" href="//api.share.zhix.net">
<link rel="dns-prefetch" href="//bdimg.share.zhix.net">
DNS用的什么网络协议
DNS 在区域传输(将一个区域文件传输到多个 DNS服务器的过程叫做区域传输)的时候用的 TCP,在域名解析的时候用的是 UDP
浏览器请求并发有限制,如何处理
- 雪碧图,把请求的icon 合并成一张图片。
- 对 js和 css打包,资源合并
- 给资源做缓存
- 图片按需加载
- 给资源做 Hash,请求到不同域下(因为只有相同域才有并发限制)
短轮询和长轮询
- 短轮询
指每隔特定时间,由浏览器对服务器发出 http 请求,然后服务端返回最新的数据给客户端
缺点:请求中大半是没用的,难于维护,浪费带宽和服务器资源;响应的结果没有顺序(因为是异步请求,如果当前请求还没返回结果时,下一个请求就请求到了,当前请求就过时无效了)
- 长轮询
客户端发起 ajax 请求后,服务端阻塞请求,等有数据或者超时的时候在返回。返回信息之后,客户端再次发起请求,重新建立连接。
缺点:返回数据顺序无保证
- 两者的共同缺点
服务器被动,不能主动推送信息。
讲一讲登录实现
用户第一次登录的时候,后端生成该用户对应的token(唯一的并且有时效性的)并返回给前端,前端收到token之后存储在localStorage里面,并且记录用户的登录状态,下次在发送用户相关的请求的时候需要携带上token(后端需要设置Access-control-allow-headers : token避免跨域问题),后端给每个用户相关的接口都加上token校验。每次用户切换界面的时候都进行路由守卫的拦截验证,如果登录状态为true,则可以访问,如果为false,则不允许访问
什么是token
用户第一次登陆的时候,后端生成该用户对应的token(唯一并且有时效性),并存在数据库里(如果太多用户登陆的话会造成大量空间浪费,可以使用JWT),并返回给前端。前端把它存储在localStorage中,下一次发送请求时(关于用户的请求,比如修改头像、密码或者自动登录)带上token并放在header中(不配合后端的话会出现跨域问题,后端需要设置Access-control-allow-headers : token),后端给每个用户相关的接口都加上验证token操作,token正确则返回对应的数据,错误则报错处理。当用户退出登录的时候删掉对应的token
CSS/HTML
常见的行内元素和块级元素都有哪些
- 行内元素inline:不能设置宽高,多个元素共享一行,占满的时候会换行;
span、input、img、textarea、label、select - 块级元素block:可以设置宽高,一个元素占满一整行
p、h1/h2/h3/h4/h5、div、ul、li、table - inline-block:可以设置宽高,多个元素共享一行,占满的时候会换行
请说明px,em,rem,vm,vh,rpx等单位的特性
- px:像素
- em:当前元素的字体大小
- rem:根元素字体大小
- vm:xm是总宽度
- vh:100vh是总高度
- rpx:750rpx是总宽度
常见的替换元素和非替换元素
- 替换元素:是指若标签的属性可以改变标签的显示方式就是替换元素,比如input的type