前端面试题库

377 阅读17分钟

HTML,CSS

1.flex布局

基本概念

容器与项目

什么叫容器 采用flex布局的元素被称作容器。

什么叫项目 在flex布局中的子元素被称作项目。

即父级元素采用flex布局,则父级元素为容器,全部子元素自动成为项目

image.png

容器的一些属性

有六个常用属性设置在容器上,分别为:

flex-direction,flex-wrap,flew-flow,justify-content,align-items,align-content

flex-direction 属性
.wrap{
    flex-direction:row | row-reverse | column | column-reverse;
}

包含四个属性值:

row: 默认值,表示沿水平方向,由左到右。

row-reverse :表示沿水平方向,由右到左

column:表示垂直方向,由上到下

column-reverse:表示垂直方向,由下到上

flex-flow属性

lex-flow属性是flex-deriction和flex-wrap属性的简写,默认值为[row nowrap];,

第一个属性值为flex-direction的属性值

第二个属性值为flex-wrap的属性值

项目的一些属性

flex-grow 属性

flex-grow属性用来控制当前项目是否放大显示。默认值为0,表示即使容器有剩余空间也不放大显示。如果设置为1,则平均分摊后放大显示

.green-item{
    flex-grow:2;
    }

image.png

flex-shrink 属性

flex-shrink属性表示元素的缩小比例。默认值为1,如果空间不够用时所有的项目同比缩小。如果一个项目的该属性设置为0,则空间不足时该项目也不缩小。

flex-basis属性

flex-basis属性表示表示项目占据主轴空间的值。默认为auto,表示项目当前默认的大小。如果设置为一个固定的值,则该项目在容器中占据固定的大小。

flex属性

flex属性是 flex-grow属性、flex-shrink属性、flex-basis属性的简写。默认值为:0 1 auto;

flex:1; 即就是代表均匀分配元素

flex: 1; === flex: 1 1 0;

第一个参数表示: flex-grow 定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大

第二个参数表示: flex-shrink 定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小

第三个参数表示: flex-basis给上面两个属性分配多余空间之前, 计算项目是否有多余空间, 默认值为 auto, 即项目本身的大小;设置为auto的时候,会根据盒子内容的多少自动撑开盒子,它里面的每个盒子的宽度是不一样的

.item{
    flex:(0 1 auto) | auto(1 1 auto) | none (0 0 auto)
}

align-self 属性

align-self属性表示当前项目可以和其他项目拥有不一样的对齐方式。它有六个可能的值。默认值为auto

auto:和父元素align-self的值一致

flex-start:顶端对齐

flex-end:底部对齐

center:竖直方向上居中对齐

baseline:item第一行文字的底部对齐

stretch:当item未设置高度时,item将和容器等高对齐

JS

1.原型与原型链

参考https://juejin.cn/post/6844903749345886216

JavaScript中一切引用类型都是对象,对象就是属性的集合。

Array类型、Function类型、Object类型、Date类型、RegExp类型等都是引用类型

也就是说 数组是对象、函数是对象、正则是对象、对象还是对象。

image.png

原型和原型链是什么

上面我们说到对象就是属性(property)的集合,有人可能要问不是还有方法吗?其实方法也是一种属性,因为它也是键值对的表现形式,具体见下图。

image.png 可以看到obj上确实多了一个sayHello的属性,值为一个函数,但是问题来了,obj上面并没有hasOwnProperty这个方法,为什么我们可以调用呢?这就引出了 原型

每一个对象从被创建开始就和另一个对象关联,从另一个对象上继承其属性,这个另一个对象就是 原型

当访问一个对象的属性时,先在对象的本身找,找不到就去对象的原型上找,如果还是找不到,就去对象的原型(原型也是对象,也有它自己的原型)的原型上找,如此继续,直到找到为止,或者查找到最顶层的原型对象中也没有找到,就结束查找,返回undefined

这条由对象及其原型组成的链就叫做原型链。

现在我们已经初步理解了原型和原型链,到现在大家明白为什么数组都可以使用push、slice等方法,函数可以使用call、bind等方法了吧,因为在它们的原型链上找到了对应的方法。

总结:

  1. 原型存在的意义就是组成原型链:引用类型皆对象,每个对象都有原型,原型也是对象,也有它自己的原型,一层一层,组成原型链。
  2. 原型链存在的意义就是继承:访问对象属性时,在对象本身找不到,就在原型链上一层一层找。说白了就是一个对象可以访问其他对象的属性。

Array Object都是构造函数 console.log('数组原型',Array.prototype)

console.log('对象原型',Object.prototype)

// 惯例,构造函数应以大写字母开头
		function Person (name) {

			this.name = name
			this.say = function () {
				console.log('Hi'+name)
			}
		}
		// 对象的创建方式主要有两种,一种是new操作符后跟函数调用,另一种是字面量表示法
		// new操作符 创建对象 

		let newPerson = new Person('小花')
		console.log('newPerson',newPerson)   //{name:'小花',f() {}}

		console.log(newPerson.name)   //小花
		console.log(newPerson.say()) //Hi小花

		// 总结一下:构造函数用来创建对象,同一构造函数创建的对象,其原型相同

		let newPerson2 =  new Person('小名')



		console.log(newPerson.prototype === newPerson2.prototype)  //true


	//Array Object都是构造函数 
	// 	对象有__proto__属性,函数有__proto__属性,数组也有__proto__属性,只要是引用类型,就有__proto__属性,指向其原型。
	// 只有函数有prototype属性,只有函数有prototype属性,只有函数有prototype属性,指向new操作符加调用该函数创建的对象实例的原型对象。

		console.log(Person.prototype === newPerson.__proto__) // true

2.js本地存储

cooKie,session,localStorage

共同点:都是保存在浏览器端、且同源的.

区别:

1、cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递,而sessionStorage和localStorage不会自动把数据发送给服务器,仅在本地保存。cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下

2、存储大小限制也不同,cookie数据不能超过4K,同时因为每次http请求都会携带cookie、所以cookie只适合保存很小的数据,如会话标识。sessionStorage和localStorage虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大

3、数据有效期不同,sessionStorage:仅在当前浏览器窗口关闭之前有效;localStorage:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie:只在设置的cookie过期时间之前有效,即使窗口关闭或浏览器关闭

4、作用域不同,sessionStorage不在不同的浏览器窗口中共享,即使是同一个页面;localstorage在所有同源窗口中都是共享的;cookie也是在所有同源窗口中都是共享的

3.常用的数组方法

01.Array.push()

向数组的末尾添加一个或多个元素,并返回新的数组长度。原数组改变

image.png

02.Array.pop()

删除并返回数组的最后一个元素,若该数组为空,则返回undefined。原数组改变

image.png

03.Array.unshift()

向数组的开头添加一个或多个元素,并返回新的数组长度。原数组改变

image.png

04.Array.shift()

删除数组的第一项,并返回第一个元素的值。若该数组为空,则返回undefined。原数组改变。

image.png

05.Array.join()

将数组的每一项用指定字符连接形成一个字符串。默认连接字符为 “,” 逗号

image.png

05.Array.slice()

参数:

array.slice(n, m),从索引n开始查找到m处(不包含m)

array.slice(n) 第二个参数省略,则一直查找到末尾

array.slice(0)原样输出内容,可以实现数组克隆

array.slice(-n,-m) slice支持负参数,从最后一项开始算起,-1为最后一项,-2为倒数第二项

返回值:返回一个新数组

是否改变原数组:不改变

image.png

06.Array.splice()

Array.splice(index,howmany,arr1,arr2…) ,用于添加或删除数组中的元素。从index位置开始删除howmany个元素,并将arr1、arr2…数据从index位置依次插入。howmany为0时,则不删除元素。
原数组改变。

image.png

4.forEach如何终止循环

forEach专门用来循环数组,可以直接取到元素,同时也可以取到index值

存在局限性,不能continue跳过或者break终止循环,没有返回值,不能return

终止foreach循环 :运用抛出异常(try catch)可以终止foreach循环

错误用法一:

var array = ["第一","第二","第三","第四"];        
// 直接就报错了
array.forEach(function(item,index){
    if (item == "第三") {
        break;
    }
    alert(item);
});

报错如下:

image.png

错误用法2:使用return fasle (只是终止本次循环)

var array = ["第一","第二","第三","第四"];
        
// 会遍历数组所有元素,只是执行跳过"第三"return false下面的代码不再执行而已
array.forEach(function(item,index){
    if (item == "第三") {
        return false;
    }
    console.log(item);// "第一" "第二" "第四"
});
console.log("以下代码")// 以下代码

image.png

正确用法:运用抛出异常(try catch)

try {
    var array = ["第一","第二","第三","第四"];
    
    // 执行到第3次,结束循环
    array.forEach(function(item,index){
        if (item == "第三") {
            throw new Error("第三");
        }
        console.log(item);// 第一 第二
    });
} catch(e) {
    if(e.message!="第三") throw e;
};
// 下面的代码不影响继续执行
console.log("下方代码");//下方代码

image.png

5.数组去重的方法

数组去重的几种方法

6.call,apply,bind的区别

参考:call,apply,bind区别,手写call,bind.apply call.bind.apply都是改变 this指向的方法

1.call

把找到的call方法执行
call方法执行的时候,内部处理了一些事情
1.首先把要操作的函数中的this关键字变为call方法第一个传递的实参
2.把call方法第二个及之后的实参获取到
3.把要操作的函数执行,并且把第二个以后传递进来的实参传递给函数

fn.call(thisArg, arg1, arg2, ...)

call细节

1.非严格模式下

如果不传参数,或者第一个参数是`null``nudefined``this`都指向`window`
    let fn = function(a,b){
        console.log(this,a,b);
    }
    let obj = {name:"obj"};
    fn.call(obj,1,2);    // this:obj    a:1         b:2
    fn.call(1,2);        // this:1      a:2         b:undefined
    fn.call();           // this:window a:undefined b:undefined
    fn.call(null);       // this=window a=undefined b=undefined
    fn.call(undefined);  // this=window a=undefined b=undefined

2.严格模式下

第一个参数是谁,this就指向谁,包括null和undefined,如果不传参数this就是undefined

    "use strict"
    let fn = function(a,b){
        console.log(this,a,b);
    }
    let obj = {name:"obj"};
    fn.call(obj,1,2);   // this:obj        a:1          b:2
    fn.call(1,2);       // this:1          a:2          b=undefined
    fn.call();          // this:undefined  a:undefined  b:undefined
    fn.call(null);      // this:null       a:undefined  b:undefined
    fn.call(undefined); // this:undefined  a:undefined  b:undefined

2.apply

apply把需要传递给fn的参数放到一个数组(或者类数组)中传递进去,虽然写的是一个数组,但是也相当于给fn一个个的传递

fn.call(obj, 1, 2);
fn.apply(obj, [1, 2]);

3.bind

call/apply 改变了函数的 this 上下文后 马上 执行该函数

bind 则是返回改变了上下文后的函数, 不执行该函数 。

4.手写实现call


const name = '李四'

const person = {
    getName:function() {
        console.log(this)
        return this.name
    }
}
const man = {
    name:'张三'
}


Function.prototype.myCall = function () {
    1.getName函数要执行
    
    const res = this()
    
    return res;
}

console.log(person.getName.myCall(man))  // 这时候this指向window,打印出李四

Function.prototype.myCall = function (context) {
    1.getName函数要执行
    1-2 这个时候getName函数的this指向的是window
    1-3 传过来的形参是我想指向的man
    this是一个函数, context就是我们想要改变指向的man对像
    给context挂在一个函数 把this赋值给他
    context.fn = this;
    const res = context.fn()
    
    return res;
}


7.数据类型

JavaScript(以下简称js)的数据类型分为两种:原始类型(即基本数据类型)和对象类型(即引用数据类型)

js常用的基本数据类型包括undefined - - (未定义)、null- - (空的)、number - - (数字)、boolean- - (布尔值)、string- - (字符串)、Symbol - - (符号);

js的引用数据类型也就是对象类型Object- - (对象),比如:array - - (数组)、function - - (函数)、date - - (时间)等;

8.判断数据类型的方式

1.typeof

  • 识别所有值类型;
  • 识别函数类型;
  • 识别引用类型,但是无法区分对象,数组以及 null
  • Infinity 和 NaN 会被识别为 number,尽管 NaN 是 Not-A-Number 的缩写,意思是"不是一个数字"
 let a
  const b = null
  const c = 100
  const d = 'warbler'
  const e = true
  const f = Symbol('f')
  const foo = () => {}
  const arr = []
  const obj = {}
  console.log(typeof a) //=> undefined
  console.log(typeof b) //=> object
  console.log(typeof c) //=> number
  console.log(typeof d) //=> string
  console.log(typeof e) //=> boolean
  console.log(typeof f) //=> symbol
  console.log(typeof foo) //=> function
  console.log(typeof arr) //=> object
  console.log(typeof obj) //=> object
  console.log(typeof Infinity) //=> number
  console.log(typeof NaN) //=> number

instanceof方法

无法区分是Array还是Object [] instenceof Object true {} instenceof Object true

用来检测引用数据类型,值类型都会返回 false

左操作数是待检测其类的对象,右操作数是对象的类。如果左侧的对象是右侧的实例,则返回 true,否则返回false。

检测所有 new 操作符创建的对象都返回 true。

检测 null 和 undefined 会返回 false

const foo = () => { }
  const arr = []
  const obj = {}
  const data = new Date()
  const number = new Number(3)
  console.log(foo instanceof Function) //=> true
  console.log(arr instanceof Array) //=> true
  console.log(obj instanceof Object) //=> true
  console.log(data instanceof Object) //=> true
  console.log(number instanceof Object) //=> true
  console.log(null instanceof Object) //=> false
  console.log(undefined instanceof Object) //=> false

Object.prototype.toString.call

对于 Object.prototype.toString() 方法,会返回一个形如 [object XXX] 的字符串。 使用Object.prototype.toString.call 的方式来判断一个变量的类型是最准确的方法。 Object.prototype.toString.call 换成 Object.prototype.toString.apply 也可以

  let a
  const b = null
  const c = 100
  const d = 'warbler'
  const e = true
  const f = Symbol('f')
  const reg = /^[a-zA-Z]{5,20}$/
  const foo = () => { }
  const arr = []
  const obj = {}
  const date = new Date();
  const error = new Error();
  const args = (function() {
    return arguments;
  })()
  console.log(Object.prototype.toString.call(a)) //=> [object Undefined]
  console.log(Object.prototype.toString.call(b)) //=> [object Null]
  console.log(Object.prototype.toString.call(c)) //=> [object Number]
  console.log(Object.prototype.toString.call(d)) //=> [object String]
  console.log(Object.prototype.toString.call(e)) //=> [object Boolean]
  console.log(Object.prototype.toString.call(f)) //=> [object Symbol]
  console.log(Object.prototype.toString.call(reg)) //=> [object RegExp]
  console.log(Object.prototype.toString.call(foo)) //=> [object Function]
  console.log(Object.prototype.toString.call(arr)) //=> [object Array]
  console.log(Object.prototype.toString.call(obj)) //=> [object Object]
  console.log(Object.prototype.toString.call(date)) //=> [object Date]
  console.log(Object.prototype.toString.call(error)) //=> [object Error]
  console.log(Object.prototype.toString.call(args)) //=> [object Arguments]

封装成简单的函数使用

const getPrototype = (item) => Object.prototype.toString.call(item).split(' ')[1].replace(']', '');
console.log(getPrototype('abc')) //=> String

9.递归算法

递归的概念

就是函数自己调用自己本身,或者在自己函数调用的下级函数中调用自己。

递归经典案例

案例1:求和

求1-100的和
function sum (n) {
    if (n == 1) return 1
    return sum(n - 1) + n
}

案例2. 斐波拉契数列

1,1,2,3,5,8,13,21,34,55,89...求第 n 项

function fib(n) {
    if (n == 1 || n == 2) return 1
    return fib(n - 1) + fib(n - 2)
}

案例3. 爬楼梯

JS 递归 假如楼梯有 n 个台阶,每次可以走 1 个或 2 个台阶,请问走完这 n 个台阶有几种走法

function climp (n) {
    if (n == 1) return 1
    if (n == 2) return 2
    return climp(n - 1) + climp(n - 2)
}

10.深拷贝,浅拷贝

浅拷贝:只是拷贝一层,更深层次对象级别的只拷贝了地址。

深拷贝:深拷贝就会拷贝多层,即使是嵌套了对象,也会都拷贝出来,内容和原对象一样,更改原对象,拷贝对象不会发生变化

1.直接赋值

	       // 直接赋值  浅拷贝
		const obj1 = {
			a:1,
			b:2,
			c:3
		}
		const obj2 = obj1;

		obj2.a = 4;
		// 直接赋值属于浅拷贝,修改obj2时  影响到obj1元数据
		console.log('obj1',obj1)  //{a:4,b:1,c:2}

2. ...展开运算符

//...展开运算符  实现深拷贝的时候  只针对基础数据类型
		const obj3 = {
			a:1,
			b:2,
			c:3
		}
		const obj4 = {...obj3}
		obj4.a = 5;
		console.log('obj3',obj3)  // {a:1,b:2,c:3} //不影响源数据

		//...展开运算符  实现浅拷贝,基础数据类型实现了深拷贝没影响源数据,引用数据类型还是浅拷贝
		const obj5 = {
			a:1,
			b:2,
			c:3,
			d:{
				e:6
			}
		}
		const obj6 = {...obj5}
		obj6.a = 5;
		obj6.d.e= 7;
		console.log('obj5',obj5)  // {a:1,b:2,c:3,d:{e:7}}   基础类型实现了深拷贝  引用类型还是浅拷贝

3.object.assign()

// object.assign(target,...sources) 

		//target 要拷贝给谁  sources 要拷贝的对象
		//object.assign 实现深拷贝 只针对基本数据类型,针对更深层次的数据 无法实现深拷贝
		const obj7 = {
			a:1,
			b:2,
			c:3
		}
		const newobj = {}
		Object.assign(newobj,obj7)
		console.log('newobj',newobj)  // {a:1,b:2,c:3}
		newobj.a = 4;
		console.log('obj7',obj7)  // {a:1,b:2,c:3}  此时实现了深拷贝
 		

 		//object.assign 实现浅拷贝  

		const obj8 = {
			a:1,
			b:2,
			c:3,
			d:{
				e:4
			}
		}
		const newobj2 = {}

		Object.assign(newobj2,obj8)

		console.log('newobj2',newobj2) // {a:1,b:2,c:3,d:{e:4}}

		newobj2.a = 5;
		newobj2.d.e = 6;

		console.log('obj8',obj8)  // {a:1,b:2,c:3,d:{e:6}}   a未改变  e改变,因为obj8.d是引用数据类型

4.JSON.stringfy JSON,parse() 实现深拷贝

const obj9 = {
			a:1,
			b:2,
			c:{
				d:4
			}
		}
		const obj10 = JSON.parse(JSON.stringify(obj9))

		console.log('obj10',obj10)  // {a:1,b:2,c:{d:4}}

		obj10.c.d = 5;
		console.log('onj9',obj9)   // {a:1,b:2,c:{d:4}}   //实现了深拷贝 没有影响到源数据

		//但是JSON.stringfy有个缺点,当要拷贝的对象含有function regExp 时间对象时  无法实现深拷贝
		//对象中有时间类型的时候,序列化之后会变成字符串类型。
		// 2. 对象中有undefined和Function类型数据的时候,序列化之后会直接丢失。
		// 3. 对象中有NaN、Infinity和-Infinity的时候,序列化之后会显示 null。
		const obj11 = {
			a:1,
			b:2,
			c:function () {
				console.log()
			}
		}
		const obj12 = JSON.parse(JSON.stringify(obj11))
		obj12.a = 'a'
		console.log('obj11',obj11)
		console.log('obj12',obj12)   //  {a:'a',b:2}  //c函数丢失

		//那么怎么避免呢?下面手写实现深拷贝


		//手写实现深拷贝  
	    const copyObj = (obj = {}) => {
    		//变量先置空
            let newobj = null;  
            //判断是否需要继续进行递归
            if (typeof (obj) == 'object' && obj !== null) {
                newobj = obj instanceof Array ? [] : {};
                //进行下一层递归克隆
                for (var i in obj) {
                    newobj[i] = copyObj(obj[i])
                }
                //如果不是对象直接赋值
            } else newobj = obj;
            
            return newobj;    
        }

		const obj13 = copyObj(obj11)
		obj13.a = 3
		console.log('obj11',obj11)   //{a:1,b:2,c:f()}
		console.log('obj13',obj13)  //{a:3,b:2,c:f()}

11.this指向

js中的this指向

1.是一个指针型变量,他动态的指向当前函数的执行环境

2.在不同的场景中调用同一个函数,this的指向也可能会发生变化,但是它永远指向其所在函数的真实调用者;如果没有调用者,就指向全局对象window

12.跨域解决

九种跨域解决方案

13.性能优化

参考:前端性能优化24条

1.减少http请求

2.使用服务端渲染

3.静态资源使用CDN

4.使用字体图标iconfont代替图片

5.图片延迟加载,降低图片质量

6.按需加载

7.减少重绘 重排

14.怎么遍历数组、对象,for in遍历对象时会不会访问原型

遍历数组:

Array.some(),Array.map(),Array.forEach(),Array.every(),Array.fllter(),Array.find(),Array.findIndex(), for of只能遍历数组,不能遍历对象,

for in遍历数组得到数组的下标,遍历对象得到对象的key

image.png

for - in特点

for-in循环会遍历原型链上可枚举的所有属性,如果不想遍历原型上的属 性,如果想要只遍历实例对象的属性,可以使用hasOwnProperty 方法过滤一下。

    let obj = {
        a: 1,
        b: 2,
        c: 3,
        __proto__: {
            lastName: 'xie'
        }
    };
    Object.prototype.addMsg = 'LM';
    for (let i in obj) (
        console.log(i),//a   b   c lastName  addMsg
            console.log(obj[i])//1   2   3  xie  LM
    );
    //现在你只想遍历obj里面的属性,原型上面的属性不需要
    for (let i in obj) {
        if (obj.hasOwnProperty(i)) {
            console.log(obj[i]);// 1  2   3
        }
    }

15.事件循环机制

动图讲解时间循环 宏任务和微任务

同步任务和异步任务

其实我们每个任务都是在做两件事情,就是发起调用得到结果

而同步任务和异步任务最主要的差别就是,同步任务发起调用后,很快就可以得到结果,而异步任务是无法立即得到结果,比如请求接口,每个接口都会有一定的响应时间,根据网速、服务器等等因素决定,再比如定时器,它需要固定时间后才会返回结果。

因此,对于同步任务和异步任务的执行机制也不同。

同步任务的执行,其实就是跟前面那个案例一样,按照代码顺序和调用顺序,支持进入调用栈中并执行,执行结束后就移除调用栈。

而异步任务的执行,首先它依旧会进入调用栈中,然后发起调用,然后解释器会将其响应回调任务放入一个任务队列,紧接着调用栈会将这个任务移除。当主线程清空后,即所有同步任务结束后,解释器会读取任务队列,并依次将已完成的异步任务加入调用栈中并执行

16.输入url到页面展示的过程

# 史上最详细的经典面试题 从输入URL到看到页面发生了什么

简单来说,共有以下几个规程

  • DNS解析
  • 发起TCP连接
  • 发送HTTP请求
  • 服务器处理请求并返回HTTP报文
  • 浏览器解析渲染页面
  • 连接结束。

17.手写防抖 节流

概念

防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时

节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效

一个经典的比喻:

想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应

假设电梯有两种运行策略 debounce 和 throttle,超时设定为15秒,不考虑容量限制

电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送,这是防抖。

电梯第一个人进来后,15秒后准时运送一次,这是节流。

防抖 只执行最后一次

因为每次都会重新let timer,所以这个时候我们就要使用闭包(延迟了变量的生命周期)了。

function debounce(fun, time) {
        let timer;
        return function(){
         // 如果之前就存在定时器,就要把之前那个定时器删除
            if (timer) {
                clearTimeout(timer)
            }
            timer = setTimeout(() => {
                fun.apply(this, arguments)
            }, time)
        }
      }
      function addOne(){
        console.log('this',this)  // <button id="btn">clickMe</button>,如果不写fun.apply(this,argmuments)  打印出的this是window  这显然不是我们要的
        console.log('增加一个')
    }
	btn.addEventListener('click', debounce(addOne,2000))

节流 只执行一次

	function scrollTest(){ 
		    console.log('现在我触发了')
		}
		let btn2 = document.getElementById('btn2')

	function throttle(func,time) {
		let t1 = 0;
		return function () {
			let t2 = new Date()
			if (t2 - t1 > time) {
				func.apply(this,arguments)
			}
			t1 = t2;
		}
	}
	btn2.addEventListener('click',throttle(scrollTest,1000) //连续触发时,在delay秒内只执行一次 

19. js内存泄漏

什么是内存泄漏?

由于疏忽或错误造成程序未能释放已经不再使用的内存

引起内存泄漏的情况

1.声明了一个全局变量,但是又没有用上,那么就有点浪费内存了

function foo(arg) {
  bar = 'this is a hidden global variable'
}

2.定时器没清除

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 处理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

如果id为 Node 的元素从DOM中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放

3.闭包

function bindEvent() {
  var obj = document.createElement('XXX')
  var unused = function () {
    console.log(obj, '闭包内引用obj obj不会被释放')
  }
  obj = null // 解决方法
}

解决内存泄露

我们编译器有一个自动的内存清理。常见的主要是引用记数 和 标记清除。 谷歌浏览器主要是用标记清除,大概流程是给每一个变量添加一个标记,通过内部算法计算引用情况,当不使用的时候就会自动清除。如果遇到定时器的话,我一般会在页面关闭的时候手动清除。如果遇到循环引用,我一般会手动把变量赋值为 null 来清除

垃圾回收机制

Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存

原理:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存

通常情况下有两种实现方式:

标记清除

引用计数

20. Promise特点,手写promise和promise.all()

Promise有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)

Promise优点

1.解决了回调地狱的问题,将异步操作以同步操作的流程表达出来

2.Promise 带来的额外好处是包含了更好的错误处理方式(包含了异常处理)

Promise缺点

1.无法取消Promise,一旦新建它就会立即执行,无法中途取消 2.如果不设置回调函数,Promise内部抛出的错误,不会反应到外部 3.当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)

21. 数组扁平化

1.flat()

const arr = [1,[2,3,[6,7]],4,5]

console.log('flat',arr.flat())   //[1,2,3,[6,7],4,5]   //不能无限扁平化
console.log('flat',arr.flat(Infinity))  //[1,2,3,6,7,4,5]   //无限扁平化

2.使用正则

const arr1 = JSON.stringify(arr).replace(/\[|\]/g,'')
console.log('arr1',JSON.parse('['+arr1+']'))

// reduce() 和concat
function flatten (arr) {
        return arr.reduce((pre,current) => {
                return pre.concat(Array.isArray(current) ? flatten(current) : current )
        },[]) 
} 

console.log('flatten',flatten(arr))

3.函数递归

function flatten(arr) {
  let result = [];

  for(let i = 0; i < arr.length; i++) {
    if(Array.isArray(arr[i])) {
      result = result.concat(flatten(arr[i]));
    } else {
      result.push(arr[i]);
    }
  }
  return result;
}
console.log(flatten(arr)); 

22. 闭包的理解,什么是闭包,闭包的应用场景,闭包的缺点

1.闭包概念

函数嵌套函数,内部函数就是闭包。 或者说如果一个函数访问了此函数的父级及父级以上的作用域变量,那么这个函数就是一个闭包

正常情况下,函数执行完,内部变量会销毁 闭包,内部函数没有执行完成,外部函数变量没有销毁。 下面就是一个简单的闭包

function outerFunc () {
    let a = 10;
    function innereFunc () {
        console.log(a)
    }
    return innerFunc()
}
let fun = outerFunc()
fun() //10

闭包的特点

1.被闭包函数访问的父级及以上的函数的局部变量(如范例中的局部变量 a )会一直存在于内存中,不会被JS的垃圾回收机制回收。

2.闭包函数实现了对其他函数内部变量的访问

闭包的用途

1.访问函数内部的变量

2.让变量始终保持在内存中

闭包的使用场景

模拟面向对象的代码风格

比如模拟两人对话

function person(name) {
    function say(content) {
         console.log(name + ':' + content)
    }
    return say
}
a = person('张三')
b = person('李四')
a("在干啥?")
b("没干啥。")
a("出去玩吗?")
b("去哪啊?")
控制台打印结果为:
张三:在干啥?
李四:没干啥。 
张三:出去玩吗? 
李四:去哪啊

通过闭包实现setTimeout第一个函数传参(默认不支持传参)

function func(param){
            alert(param)
    }
    var f1 = func(1);
    setTimeout(f1,3000);   //不使用闭包 会立即alert  不会延迟3s
    
    //使用闭包 延迟3s弹出
    	function func(param){
                return function(){
                    alert(param)
                }
            }
        var f1 = func(1);
        setTimeout(f1,3000);

封装私有变量

用闭包定义能访问私有函数和私有变量的公有函数。

var counter = (function () {
    var privateCounter = 0; //私有变量
    function change(val) {
        privateCounter += val;
    }
    return {
        increment: function () {
            change(1);
        },
        decrement: function () {
            change(-1);
        },
        value: function () {
            return privateCounter;
        }
    };
})();

console.log(counter.value());//0
counter.increment();
console.log(counter.value());//1
counter.increment();
console.log(counter.value());//2

模拟块作用域

image.png

以此点击4个li,结果都弹出4

解析:onclick绑定的function中没有变量 i,解析引擎会寻找父级作用域,最终找到了全局变量 i,for循环结束时,i 的值已变成了4,所以onclick事件执行时,全都弹出 4

下面使用闭包来解决这个问题:

var elements = document.getElementsByTagName('li');
var length = elements.length;
for(var i = 0; i < length;i ++) {
    elements[i].onclick = function (num) {
        return function () {
            alert(num)
        }
    }(i)
}
通过匿名闭包,把每次的 i 都保存到一个变量中,实现了预期效果。

闭包的优点:

可以减少全局变量的定义,避免全局变量的污染

能够读取函数内部的变量

在内存中维护一个变量,可以用做缓存

闭包的缺点:

1)造成内存泄露(只在iE中)

闭包会使函数中的变量一直保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。

解决方法——使用完变量后,手动将它赋值为null;

2)闭包可能在父函数外部,改变父函数内部变量的值。

3)造成性能损失

由于闭包涉及跨作用域的访问,所以会导致性能损失。

各自独立的闭包

function outerFn(){
  var i = 0; 
  function innerFn(){
      i++;
      console.log(i);
  }
  return innerFn;
}
var inner = outerFn();  //每次外部函数执行的时候,都会开辟一块内存空间,外部函数的地址不同,都会重新创建一个新的地址
inner();
inner();
inner();
var inner2 = outerFn();
inner2();
inner2();
inner2();   //1 2 3 1 2 3


function fn(){
	var a = 3;
	return function(){
		return  ++a;                                     
	}
}
alert(fn()());  //4
alert(fn()());  //4    


访问全局变量的闭包

var i = 0;
function outerFn(){
  function innnerFn(){
       i++;
       console.log(i);
  }
  return innnerFn;
}
var inner1 = outerFn();
var inner2 = outerFn();
inner1();
inner2();
inner1();
inner2();     //1 2 3 4

23. 箭头函数和普通函数的区别

1.箭头函数比普通函数更加简洁

如果没有参数,就直接写一个空括号即可 如果只有一个参数,可以省去参数的括号 如果函数体的返回值只有一句,可以省略大括号

2.箭头函数没有自己的this

箭头函数不会创建自己的this, 所以它没有自己的this,它只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它在定义时已经确定了,之后不会改变。

箭头函数的this指向的是在你书写代码时候的上下文环境对象的this,如果没有上下文环境对象,那么就指向最外层对象window

image.png

3.箭头函数继承来的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'

对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象

4. call()、apply()、bind()等方法不能改变箭头函数中this的指向

var id = 'Global';
let fun1 = () => {
    console.log(this.id)
};
fun1();                     // 'Global'
fun1.call({id: 'Obj'});     // 'Global'
fun1.apply({id: 'Obj'});    // 'Global'
fun1.bind({id: 'Obj'})();   // 'Global'

5.箭头函数没有自己的arguments

箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。

image.png

箭头函数的this指向哪⾥?

箭头函数不同于传统JavaScript中的函数,箭头函数并没有属于⾃⼰的this,它所谓的this是捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值,并且由于没有属于⾃⼰的this,所以是不会被new调⽤的,这个所谓的this也不会被改变。

24. 柯里化是什么,有什么用,怎么实现

1.什么是函数柯里化?

在计算机科学中,柯里化(英语:Currying ),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数,而且返回结果的新函数的技术。

用大白话来说就是只传递给函数一部分参数来调用它,让它返回一个新函数去处理剩下的参数

2.简单的柯里化实现

没有柯里化实现的案例

image.png

将其转化为柯里化的案例

image.png

自动柯里化

function myCurried(fn) {
  return function curry(...args1) {
    if (args1.length >= fn.length) {
      return fn.call(null, ...args1)
    } else {
      return function (...args2) {
        return curry.apply(null, [...args1, ...args2])
      }
    }
  }
}

function sum(a, b, c, d, e) {
  return a + b + c + d + e
}
let resFunc = myCurried(sum)
console.log(resFunc(1,3,4)(1)(23))
//解析:
//1、这里的fn.length获取的是函数传入参数的长度
//2、这里使用递归的思想

柯里化的作用

单一原则:在函数式编程中,往往是让一个函数处理的问题尽可能单一,而不是一个函数处理多个任务。

提高维护性以及降低代码的重复性

柯里化的场景

1、比如我们在求和中,以一定的数字为基数进行累加的时候,就用到了函数柯里化。当然函数柯里化感觉上是把简答的问题复杂化了,其实不然。比如:

// 比如,基础分值是30 + 30;
const fractionFn = (x) => {
  const totalFraction =  x + x;
  return function(num) {
    return totalFraction + num;
  }
};
​
const baseFn = fractionFn(30);
const base1Fn = baseFn(1);
const base2Fn = baseFn(2);
console.log(base2Fn)   //62

这样来进行累加的话,是不是就简单、清晰明了呢.

我们常用的日志输出,是不是都是具体的日期、时间以及加上具体的原因呢。

其实date, type每次还需要传参。是不是可以进行抽离呢,当然了,函数柯里化就可以完美的解决这个。

                const  date = new Date()

		const logFn = (date) => (type) => (msg) => {
			return `${date.getHours()}:${date.getMinutes()} ${type} - ${msg}`
		}  
		const newLogfn = logFn(date)('warning')('变量未声明')
		console.log('newLogfn',newLogfn)  // newLogfn 10:40 warning - 变量未声明

其他使用场景 表单正则验证

在 react 项目中使用 antd 表单的时候,遇到一些老项目,需要校验密码的强弱、校验输入的规则等,如果每次都是传正则和需要校验的字符串,有点麻烦。

import React from "react";
​
const accountReg = /^[a-zA-Z0-9_-]{4,16}$/;
const passwordReg = /^(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)(?=.*?[!@#$])[a-zA-Z\d!@#$]{10,16}$/;
​
const FormCom = () => {
​
  const checkReg = (reg, txt) => {
    return reg.test(txt)
  }
  
  //账号
  const checkAccount = (event) => {
    checkReg(accountReg, event.target.value);
    // 其他逻辑
  };
​
  //密码
  const checkPassword = (event) => {
    checkReg(passwordReg, event.target.value);
    // 其他逻辑
  };
 
  ...
  // 省去其他函数校验
  
  render() {
    return (
      <form>
        账号:
        <input onChange={checkAccount} type="text" name="account" />
        密码:
        <input onChange={checkPassword} type="password" name="password" />
      </form>
    );
  }
}
​
export default FormCom;

我们怎么解决类似的问题呢,我们可以使用柯里化函数来解决类似的问题

​
const accountReg = /^[a-zA-Z0-9_-]{4,16}$/;
const passwordReg = /^(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)(?=.*?[!@#$])[a-zA-Z\d!@#$]{10,16}$/;
​
const FormCom = () => {
  
  // 柯里化封装
  const curryCheck = (reg) => {
    return function(txt) {
        return reg.test(txt)
    }
  }
​
  //账号,这样就省去了一个参数的传递
  const checkAccount = curryCheck(accountReg);
​
  //密码,这样就省去了一个参数的传递
  const checkPassword = curryCheck(passwordReg);
​
  const checkAccountFn = () => {
    checkAccount(event.target.value);
    // 其他逻辑
  }
 
  const passwordFn = (event) => {
    checkPassword(event.target.value);
    // 其他逻辑
  };
  
  ...
  // 省去其他函数校验
​
  render() {
    return (
      <form>
        账号:
        <input onChange={checkAccountFn} type="text" name="account" />
        密码:
        <input onChange={passwordFn} type="password" name="password" />
      </form>
    );
  }
}
​
export default FormCom;

25. common.js和esm区别

CommonJS

CommonJs可以动态加载语句,代码发生在运行时

let lists = ["./index.js", "./config.js"]
lists.forEach((url) => require(url)) // 动态导入

if (lists.length) {
    require(lists[0]) // 动态导入
}

CommonJs导出值是拷贝,可以修改导出的值,这在代码出错时,不好排查引起变量污染 CommonJs导入的值是拷贝的,所以可以修改拷贝值,但这会引起变量污染,一不小心就重名

// index.js
let num = 0;
module.exports = {
    num,
    add() {
       ++ num 
    }
}

let { num, add } = require("./index.js")
console.log(num) // 0
add()
console.log(num) // 0
num = 10

上面example中,可以看到exports导出的值是值的拷贝,更改完++ num值没有发生变化,并且导入的num的值我们也可以进行修改

es module

混合导入:

import语句必须先是默认导出,后面再是单个导出,顺序一定要正确否则报错。

// index,js
export const name = "蛙人"
export const age = 24
export default {
    msg: "蛙人"
}

import msg, { name, age } from './index.js'
console.log(msg) // { msg: "蛙人" }

Es Module是静态的,不可以动态加载语句,只能声明在该文件的最顶部,代码发生在编译时

就是Es Module语句``import只能声明在该文件的最顶部,不能动态加载语句,Es Module`语句运行在代码编译时

if (true) {
	import xxx from 'XXX' // 报错
}

Es Module混合导出,单个导出,默认导出,完全互不影响

Es Module导出是引用值之前都存在映射关系,并且值都是可读的,不能修改

export导出的值是值的引用,并且内部有映射关系,这是export关键字的作用。而且导入的值,不能进行修改也就是只读状态

// index.js
export let num = 0;
export function add() {
    ++ num
}

import { num, add } from "./index.js"
console.log(num) // 0
add()
console.log(num) // 1
num = 10 // 抛出错误

26. 使用new创建对象的过程

27 let const var区别

28 ES5继承和Class继承区别

VUE

1.VUe声明周期,作用

beforeCreate:是 new Vue() 之后触发的第一个钩子,在当前阶段 datamethodscomputed 以及 watch 上的数据和方法都不能被访问

created:在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发 updated 函数。可以做一些初始数据的获取,在当前阶段无法与 Dom 进行交互,如果非要想,可以通过 vm.$nextTick 来访问 Dom

beforeMount:发生在挂载之前,在这之前 template 模板已导入渲染函数编译。而当前阶段虚拟 Dom 已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发 updated

mounted:在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点,使用 $refs 属性对 Dom 进行操作。

2.VUE组件通信方式?兄弟组件的通信方式

父子组件通信

1.props $emit
2.$parent  $child 
3.$attr  $listeners
4.provide inject

兄弟组件通信

1.eventBus 
2.vuex 

3.computed和watch的区别

computed

1.它支持缓存,只有他赖的数据发生变化,才会重新计算

2.不支持异步,当computed有异步操作时,无法监听数据的变化

watch

1.不支持缓存,当数据发生变化时,就会触发相应的操作

2.支持异步监听

3.监听的函数接收2个参数,一个是新的值,一个变化之前的值

4.监听数据必须是 data 中声明的或者父组件传递过来的 props 中的数据,当发生变化时,会触发其他操作

函数接收2个参数

immediate:组件加载立即触发回调函数

deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化

4.vuex

# 一文学会使用Vuex

5.第一次页面加载会触发哪几个钩子

beforeCreate created beforeMount mounted

6.在哪个生命周期中发起数据请求

可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。

推荐在 created 钩子函数中调用异步请求,有以下优点:

  • 能更快获取到服务端数据,减少页面 loading 时间;
  • ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;

7.vue优缺点

渐进式框架:可以在任何项目中轻易的引入

双向数据绑定:操作数据更加方便

组件化:很大程度上实现了逻辑的封装和重用,在构建单页面应用方面有着独特的优势

视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;

8.什么是虚拟dom?

Virtual DOMDOM节点在 JavaScript 中的一种抽象数据结构,之所以需要虚拟 DOM,是因为浏览器中操作 DOM 的代价比较昂贵,频繁操作 DOM 会产生性能问题。

虚拟 DOM 的作用是在每一次响应式数据发生变化引起页面重渲染时,Vue 对比更新前后的虚拟 DOM,匹配找出尽可能少的需要更新的真实 DOM,从而达到提升性能的目的。

9.如何解决 vue 初始化页面闪动问题

使用 vue 开发时,在 vue 初始化之前,由于 div 是不归 vue 管的,所以我们写的代码在还没有解析的情况下会容易出现花屏现象,看到类似于 {{message}} 的字样,虽然一般情况下这个时间很短暂,但是我们还是有必要让解决这个问题的。

首先:在 css 里加上 [v-cloak] { display: none; } 。如果没有彻底解决问题,则在根元素加上 style="display: none;" :style="{display:  block }"

10.什么是 SPA,有什么优点和缺点

SPA 仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。

优点:

用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染; 有利于前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;

缺点:

初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载; 不利于 SEO:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。

11.vue2和vue3的区别

响应式原理

生命周期钩子名称

自定义指令钩子名称

新的内置组件

diff 算法

Composition API

12.watch watchEffect区别

watch和watchEffect

用了一遍watch和watchEffect之后,发现他俩主要有以下几点区别:

1.watch是惰性执行的,而watchEffect不是,不考虑watch第三个配置参数的情况下,watch在组件第一次执行的时候是不会执行的,只有在之后依赖项变化的时候再执行,而watchEffect是在程序执行到此处的时候就会立即执行,而后再响应其依赖变化执行。 2.watch需要传递监听的对象,watchEffect不需要

vue3 数组直接赋值

1.把数组放到obj里面

const tableData = reactive({
    arr: [ ]
}) //表格数据

const res = [1,2,3] //假设是接口返回的数据
tableData.arr = res

uniapp和微信原生小程序的区别

uniapp优点

可以使用sass less编译语言,使用vue语法,编写方便,可以使用vue的计算属性,可以使用vuex

区别

传参方式不同

微信小程序使用data-属性,uniapp可直接传参

input的value值绑定并监听

uniapp适应v-model,原生写法<input value='{{sex}}' bindinput='jianting'></input>

小程序js和页面的渲染机制

渲染流程

1.在渲染层,宿主环境会把wxml转换成对应的JS对象(宿主环境指的就是微信客户端)

2.将JS对象再次转换成真实DOM树,交由渲染层线程渲染

3.数据变化时,逻辑层提供最新的变化数据,生成新的JS对象与之前的JS对象进行diff算法对比

4.将最新变化的内容反映到真实的DOM树中,更新UI

通讯模型

通讯模型是多线程的:
小程序的渲染层和逻辑层分别由2个线程管理:
渲染层的界面使用了WebView 进行渲染;
逻辑层采用JsCore线程运行JS脚本,js仍是单线程。

vue3diff算法