1、js常用写法
1、扁平化
function flatArr(arr){
return arr.reduce((cal, cur) => Array.isArray(cur) ?
cal.concat(flatArr(cur)) : cal.concat(cur)
}
2、类数组变成数组
类数组是具有length属性,但不具有数组原型上的方法。比如 arguments,DOM操作返回的结果就是类数组。将类数组变为数组的方法:
Array.from(document.querySelectorAll("div"))
Array.prototype.slicec.call(document.querySelectorAll("div"))
[...document.querySelectorAll("div")]
3、删除数组成员
//删除成员同时不改变后面成员的索引值
delete arr[index]
//删除成员的同时,删除所占位置的使用,后面成员会自动补上了
arr.splice(index,1)
4、for..of for...in
for..in 用于遍历对象;如果for..in遍历数组的话,会把数组原型上的方法以及数组的属性遍历出 来;
for..of 用于遍历数组;遍历数组不会遍历数组原型上的方法以及数组的属性遍历处理;for..of可 以遍历具有迭代器对象的集合,如Map、Set以及字符串
5、数组(array)
-
map: 遍历数组,返回回调返回值组成的新数组 -
forEach: 无法break,可以用try/catch中throw new Error来停止;仅仅是循环数组 -
filter: 过滤 -
some: 有一项返回true,则整体为true -
every: 有一项返回false,则整体为false -
join: 通过指定连接符生成字符串 -
push / pop: 末尾推入和弹出,改变原数组,push返回数组长度,pop返回原数组最后一项; -
unshift / shift: 头部推入和弹出,改变原数组,unshift返回数组长度,shift返回原数组第一项 ; -
sort(fn) / reverse: 排序与反转,改变原数组 -
concat: 连接数组,不影响原数组, 浅拷贝 -
slice(start, end): 返回截断后的新数组,不改变原数组 -
splice(start, number, value...): 返回删除元素组成的数组,value 为插入项,改变原数组 -
indexOf / lastIndexOf(value, fromIndex): 查找数组项,返回对应的下标 -
reduce / reduceRight(fn(prev, cur), defaultPrev): 两两执行,prev 为上次化简函数的return值,cur 为当前值- 当传入
defaultPrev时,从第一项开始; - 当未传入时,则为第二项
- 当传入
-
数组乱序:
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; arr.sort(function () { return Math.random() - 0.5; }); -
数组拆解: flat: [1,[2,3]] --> [1, 2, 3]
Array.prototype.flat = function() { return this.toString().split(',').map(item => +item ) }
6、对象常用方法
- obj.hasOwnProperty(prop)返回boolean,prop:要检测的属性的 String 字符串形式表示的名称,或者 Symbol。指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)
- Object.assign(target, ...sources)返回目标对象,用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象,浅拷贝
- Object.is(value1, value2)返回boolean,判断两个值是否是相同的值,不会隐式转换
- Object.values(obj)返回对象的所有可枚举属性值的数组,值的顺序与使用for...in循环的顺序相同 ( 区别在于 for-in 循环枚举原型链中的属性 )
- Object.keys(obj)返回对的所有可枚举属性的字符串数组,排列顺序和 for...in 循环遍历时顺序一致
- Object.entries(obj)返回给定对象自身可枚举属性的键值对数组,其排列与 for...in 循环遍
- Object.getOwnPropertyNames(obj)返回对象的所有属性名字符串组成的数组,返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组
- Object.isFrozen(obj)返回boolean,判断一个对象是否被冻结
- Object.freeze(obj)返回冻结的对象,冻结一个对象。一个被冻结的对象再也不能被修改
(完整版)JS对象方法——速记小本子 (juejin.cn)
7、字符常用方法
字符串查找
- str.charAt(index)从一个字符串中返回指定索引的字符,index介于0-length-1
- str.endsWith(searchString[, length])返回boolean,判断当前字符串是否是以另外一个给定的子字符串“结尾”的,length:作为 str 的长度。默认值为 str.length。
- str.includes(searchString[, position])返回boolean,判断一个字符串是否包含在另一个字符串中,从postition索引开始搜寻,默认0
- str.indexOf(searchValue [, fromIndex])返回第一次出现的索引,没有出现则为-1,fromIndex小于0则返回null,大于length返回-1
- str.lastIndexOf(searchValue[, fromIndex])返回从字符串尾部开始第一次出现的索引,没有则-1,fromIndex的值相对于从尾部开始的索引
- str.match(regexp)返回值:如果g模式返回全部匹配结果,不会捕获,如果非g模式,返回第一个匹配结果及其捕获组,regexp正则表达式对象
- str.matchAll(regexp)返回一个所有匹配的结果及分组捕获组的迭代器,迭代器只能使用一次
- str.search(regexp)返回首次匹配到的索引,没有则-1,执行正则表达式和 String 对象之间的一个搜索匹配
- str.startsWith(searchString[, position])返回boolean,判断str是否以另外一个子字符串pos位置开头,pos为开始搜索的位置,默认从str头部开始
字符串操作
- str.trim()返回去掉两端空白后的新字符串
- str.trimEnd/trimRight()返回去除末(右)端空白的新字符串
- str.trimStart/trimLeft()返回去除开头(左)端空格的新字符串
- str.split([separator[, limit]])返回一个以指定分隔符出现位置分隔而成的一个数组,数组元素不包含分隔符,limit限制返回的分割片段(默认全返回)
- str.slice(beginIndex[, endIndex])返回新字符串,从原str中返回beagin索引到end(不包含)索引(默认到尾部)的新字符串
- str.padEnd(targetLength [, padString])返回新字符串,用一个字符串填充当前字符串(可重复),返回填充后达到指定长度的字符串,从尾部开始填充
- str.padStart(targetLength [, padString])返回新字符串,用另一个字符串填充当前字符串(可重复),直到给定的长度。填充从当前字符串头部开始
- str.repeat(count)返回一个新字符串,重复str字符串count次,返回拼接之后的新字符串
- str.replace(regexp|substr, newSubStr|function)返回新字符串,参数1:如果是正则会匹配所有满足匹配条件的内容,如果是字符串则只会匹配第一个满足的内容。参数2:如果是字符串则此串会替换参数1匹配到的内容,如果是func,将会把func的返回值替换匹配的内容
- str.concat(string2, string3[, ..., stringN])返回新的字符串,将一个或多个字符串与原字符串连接合并
- str.substring(indexStart[, indexEnd])返回新字符串,返回一个字符串在开始索引到结束索引(不包括)之间的一个子集
字符串转换
-
str.toLowerCase()返回新字符串,将调用该方法的字符串值转为小写形式,并返回
-
str.toUpperCase()返回一个新字符串,将调用字符串转换为大写形式返回
2、节流和防抖
两者区别
节流:高频事件触发,在n秒内只会执行一次,所以节流会稀释函数的执行频率。
防抖:高频事件触发后n秒内只会执行一次,如果n秒内高频事件再次触发,则重新计算时间。
两者的区别是:函数节流是固定时间做某一件事,比如每隔1秒发一次请求;而函数防抖是在频繁触发后,只执行一次。
节流
高频事件触发,无论多频繁触发在n秒内只会执行一次,所以节流会稀释函数的执行频率。
scroll加载更多可以用到节流
<input type="text" id="search" value="12">
function jieliu(fn, duration, isFirst) {
let timer;
return function (...args) {
if(isFirst){
isFirst = false;
fn.apply(this, args);
return;
}
if(!timer){
timer = setTimeout(()=>{
fn.apply(this, args);
timer = null;
}, duration)
}
}
}
document.getElementById('search').addEventListener("input", jieliu((e)=>{
console.log(e.target.value);
}, 1000, true))
防抖
你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行,真是任性呐! 常见的例子:有一个搜索输入框,为了提升用户体验,希望在用户输入后可以立即展现搜索结果,而不是每次输入完后还要点击搜索按钮
<input type="text" id="search">
function fangdou(fn, duration) {
let timer;
return function (...args) {
if(timer){
clearTimeout(timer);
}
timer = setTimeout(()=>{
fn.apply(this, args);
}, duration)
}
}
document.getElementById('search').addEventListener('input', fangdou((e)=>{
console.log(e.target.value)
}, 1000));
3、类型判断
typeof
- 用来判断数据类型,返回值为6个字符串,分别是string, boolean, number, function, object, undefined
- 优点:能够快速区分基本数据类型 缺点:不能将Object、Array和Null区分,都返回object
instanceof
- 用来判断数据类型, obj1 instanceof obj2 返回true或者false
- 优点:能够区分Array、Object和Function,适合用于判断自定义的类实例对象 缺点:Number,Boolean,String基本数据类型不能判断
Object.prototype.toString.call()
var toString = Object.prototype.toString;
console.log(toString.call(2)); //[object Number]
console.log(toString.call(true)); //[object Boolean]
console.log(toString.call('str')); //[object String]
console.log(toString.call([])); //[object Array]
console.log(toString.call(function(){})); //[object Function]
console.log(toString.call({})); //[object Object]
console.log(toString.call(undefined)); //[object Undefined]
console.log(toString.call(null)); //[object Null]
优点:精准判断数据类型 缺点:写法繁琐不容易记,推荐进行封装后使用
4、null undefined
一般null用来给引用对象初始值;undefined一般用来判断某个对象有没有某个属性
Undefined类型只有一个值,即undefined。当声明的变量还未被初始化时,变量的默认值为undefined。用法:
- 变量被声明了,但没有赋值时,就等于undefined。
- 调用函数时,应该提供的参数没有提供,该参数等于undefined。
- 对象没有赋值的属性,该属性的值为undefined。
- 函数没有返回值时,默认返回undefined。
Null类型也只有一个值,即null。null用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象。用法
-
作为函数的参数,表示该函数的参数不是对象。
-
作为对象原型链的终点。
5、说说前端中的事件流
一、事件流
HTML中与javascript交互是通过事件驱动来实现的,例如鼠标点击事件onclick、页面的滚动事件onscroll等等,可以向文档或者文档中的元素添加事件侦听器来预订事件。想要知道这些事件是在什么时候进行调用的,就需要了解一下“事件流”的概念。
什么是事件流:事件流描述的是从页面中接收事件的顺序,DOM2级事件流包括下面几个阶段。
- 事件捕获阶段
- 处于目标阶段
- 事件冒泡阶段
addEventListener:addEventListener 是DOM2 级事件新增的指定事件处理程序的操作,这个方法接收3个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。
IE只支持事件冒泡。
二. 如何让事件先冒泡后捕获
在DOM标准事件模型中,是先捕获后冒泡。但是如果要实现先冒泡后捕获的效果,对于同一个事件,监听捕获和冒泡,分别对应相应的处理函数,监听到捕获事件,先暂缓执行,直到冒泡事件被捕获后再执行捕获之间。
三、事件委托
事件委托能避免对每个元素添加事件监听器。原理是:利用事件冒泡,将子元素的事件统一在父元素进行处理。父元素根据判断事件来源确定是哪个子元素触发,分开进行不同的处理。
详细说明:juejin.im/post/684790…
6、new操作符做了什么
- 首先内部创建了一个空对象,obj
- 将新对象的__proto__指向构造函数的prototype对象
- 将构造函数的作用域赋值给新的对象(也就是this指向新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回apply执行的结果或者返回新对象
7. babel编译原理
- babylon 将 ES6/ES7 代码解析成 AST
- babel-traverse 对 AST 进行遍历转译,得到新的 AST
- 新 AST 通过 babel-generator 转换成 ES5
8、函数柯里化
在一个函数中,首先填充几个参数,然后再返回一个新的函数的技术,称为函数的柯里化。通常可用于在不侵入函数的前提下,为函数 预置通用参数,供多次重复调用。
const add = function add(x) {
return function (y) {
return x + y
}
}
const add1 = add(1)
add1(2) === 3
add1(20) === 21
10、原型相关
原型 / 构造函数 / 实例
-
原型
(prototype): 一个简单的对象,用于实现对象的 属性继承。可以简单的理解成对象的爹。在 Firefox 和 Chrome 中,每个JavaScript对象中都包含一个__proto__(非标准)的属性指向它爹(该对象的原型),可obj.__proto__进行访问。 -
构造函数: 可以通过
new来 新建一个对象 的函数。 -
实例: 通过构造函数和
new创建出来的对象,便是实例。 实例通过__proto__指向原型,通过constructor指向构造函数。
说了一大堆,大家可能有点懵逼,这里来举个栗子,以Object为例,我们常用的Object便是一个构造函数,因此我们可以通过它构建实例。
// 实例
const instance = new Object()
则此时, 实例为instance, 构造函数为Object,我们知道,构造函数拥有一个prototype的属性指向原型,因此原型为:
// 原型
const prototype = Object.prototype
这里我们可以来看出三者的关系:
实例.__proto__ === 原型
原型.constructor === 构造函数
构造函数.prototype === 原型
// 这条线其实是是基于原型进行获取的,可以理解成一条基于原型的映射线
// 例如:
// const o = new Object()
// o.constructor === Object --> true
// o.__proto__ = null;
// o.constructor === Object --> false
// 注意: 其实实例上并不是真正有 constructor 这个指针,它其实是从原型链上获取的
// instance.hasOwnProperty('constructor') === false
实例.constructor === 构造函数
原型链
原型链是由原型对象组成,每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型,__proto__ 将对象连接起来组成了原型链。是一个用来实现继承和共享属性的有限的对象链。
-
属性查找机制: 当查找对象的属性时,如果实例对象自身不存在该属性,则沿着原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查找,直至最顶级的原型对象
Object.prototype,如还是没找到,则输出undefined; -
属性修改机制: 只会修改实例对象本身的属性,如果不存在,则进行添加该属性,如果需要修改原型的属性时,则可以用:
b.prototype.x = 2;但是这样会造成所有继承于该对象的实例的属性发生改变。
一. 原型链继承
原型链继承的原理很简单,直接让子类的原型对象指向父类实例,当子类实例找不到对应的属性和方法时,就会往它的原型对象,也就是父类实例上找,从而实现对父类的属性和方法的继承
// 父类
function Parent() {
this.name = '写代码像'
}
// 父类的原型方法
Parent.prototype.getName = function() {
return this.name
}
// 子类
function Child() {}
// 让子类的原型对象指向父类实例, 这样一来在Child实例中找不到的属性和方法就会到
// 原型对象(父类实例)上寻找
Child.prototype = new Parent()
// 根据原型链的规则,顺便绑定一下constructor, 这一步不影响继承, 只是在用到constructor时会需要Child.prototype.constructor = Child
Child.prototype.constructor = Child // 然后Child实例就能访问到父类及其原型上的name属性和getName()方法
const child = new Child()
child.name // '写代码像'
child.getName() // '写代码像'
原型继承的缺点:
- 由于所有Child实例原型都指向同一个Parent实例, 因此对某个Child实例的父类引用类型变量修改会影响所有的Child实例
- 在创建子类实例时无法向父类构造传参, 即没有实现
super()的功能
二. 构造函数继承
构造函数继承,即在子类的构造函数中执行父类的构造函数,并为其绑定子类的this,让父类的构造函数把成员属性和方法都挂到子类的this上去,这样既能避免实例之间共享一个原型实例,又能向父类构造方法传参
function Parent(name) {
this.name = [name]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
// 执行父类构造方法并绑定子类的this, 使得父类中的属性能够赋到子类的this上 Parent.call(this, 'zhangsan')
}
//测试
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name) // ['foo']
console.log(child2.name) // ['zhangsan']
// 报错,找不到getName(), 构造函数继承的方式继承不到父类原型上的属性和方法child2.getName()
构造函数继承的缺点:
- 继承不到父类原型上的属性和方法
三. 组合式继承
既然原型链继承和构造函数继承各有互补的优缺点, 那么我们为什么不组合起来使用呢, 所以就有了综合二者的组合式继承
function Parent(name) {
this.name = [name]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
// 构造函数继承
Parent.call(this, 'zhangsan')
}
//原型链继承
Child.prototype = new Parent()
Child.prototype.constructor = Child
//测试
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name) // ['foo']
console.log(child2.name) // ['zhangsan']
child2.getName() // ['zhangsan']
组合式继承的缺点:
- 每次创建子类实例都执行了两次构造函数(
Parent.call()和new Parent()),虽然这并不影响对父类的继承,但子类创建实例时,原型中会存在两份相同的属性和方法,这并不优雅
四. 寄生式组合继承
为了解决构造函数被执行两次的问题, 我们将指向父类实例改为指向父类原型, 减去一次构造函数的执行
function Parent(name) {
this.name = [name]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
// 构造函数继承
Parent.call(this, 'zhangsan')
}
//原型链继承
// Child.prototype = new Parent()
//将`指向父类实例`改为`指向父类原型`
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
//测试
const child = new Child()
const parent = new Parent()
child.getName() // ['zhangsan']
parent.getName() // 报错, 找不到getName()
11、闭包
一句话可以概括:闭包就是能够读取其他函数内部变量的函数,或者子函数在外调用,子函数所在的父函数的作用域不会被释放。
闭包属于一种特殊的作用域,称为 静态作用域。它的定义可以理解为: 父函数被销毁 的情况下,返回出的子函数的[[scope]]中仍然保留着父级的单变量对象和作用域链,因此可以继续访问到父级的变量对象,这样的函数称为闭包。
-
函数的一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
-
闭包会产生一个很经典的问题:
- 多个子函数的
[[scope]]都是同时指向父级,是完全共享的。因此当父级的变量对象被修改时,所有子函数都受到影响。
- 多个子函数的
-
解决:
-
变量可以通过 函数参数的形式 传入,避免使用默认的
[[scope]]向上查找 -
使用
setTimeout包裹,通过第三个参数传入 -
使用 块级作用域,让变量成为自己上下文的属性,避免共享
-
12、改变函数内部this指针的指向函数(bind,apply,call的区别)
this 指向
谁调用了函数,this 指向谁。例如:
-
obj.fn(),便是obj调用了函数,既函数中的this === obj -
fn(),这里可以看成window.fn(),因此this === window -
通过apply和call改变函数的this指向,他们两个函数的第一个参数都是一样的表示要改变指向的那个对象,第二个参数,apply是数组,而call则是arg1,arg2...这种形式。
-
通过bind改变this作用域会返回一个新的函数,这个函数不会马上执行。
Demos:
var name = 'window';
var sayName = function (param) {
console.log('my name is:' + this.name + ',my param is ' + param)
};
//my name is:window,my param is window param
sayName('window param')
var callObj = {
name: 'call'
};
//my name is:call,my param is call param
sayName.call(callObj, 'call param');
var applyObj = {
name: 'apply'
};
//my name is:apply,my param is apply param
sayName.apply(applyObj, ['apply param']);
var bindObj = {
name: 'bind'
}
var bindFn = sayName.bind(bindObj, 'bind param')
//my name is:bind,my param is bind param
bindFn();
call()的原理比较简单,由于函数的this指向它的直接调用者,我们变更调用者即完成this指向的变更:
//变更函数调用者示例
function foo() {
console.log(this.name)
}
// 测试
const obj = {
name: '写代码像'
}
obj.foo = foo // 变更foo的调用者
obj.foo() // '写代码像'
call()手写
Function.prototype.myCall = function(thisArg, ...args) {
const fn = Symbol('fn') // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
thisArg = thisArg || window // 若没有传入this, 默认绑定window对象
thisArg[fn] = this // this指向调用call的对象,即我们要改变this指向的函数
const result = thisArg[fn](...args) // 执行当前函数
delete thisArg[fn] // 删除我们声明的fn属性
return result // 返回函数执行结果
}
//测试
foo.myCall(obj) // 输出'写代码像'
apply手写
apply() 方法调用一个具有给定this值的函数,以及作为一个数组(或类似数组对象)提供的参数。
语法:func.apply(thisArg, [argsArray])
apply()和call()类似,区别在于call()接收参数列表,而apply()接收一个参数数组,所以我们在call()的实现上简单改一下入参形式即可
Function.prototype.myApply = function(thisArg, args) {
const fn = Symbol('fn') // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
thisArg = thisArg || window // 若没有传入this, 默认绑定window对象
thisArg[fn] = this // this指向调用call的对象,即我们要改变this指向的函数
const result = thisArg[fn](...args) // 执行当前函数(此处说明一下:虽然apply()接收的是一个数组,但在调用原函数时,依然要展开参数数组。可以对照原生apply(),原函数接收到展开的参数数组)
delete thisArg[fn] // 删除我们声明的fn属性
return result // 返回函数执行结果
}
//测试
foo.myApply(obj, []) // 输出'写代码像'
bind手写
Function.prototype.bind=function(obj,arg){
var arg=Array.prototype.slice.call(arguments,1);
var context=this;
return function(newArg){
arg=arg.concat(Array.prototype.slice.call(newArg));
return context.apply(obj,arg);
}
}
13、==和===区别
- ==, 两边值类型不同的时候,要先进行类型转换,再比较
- ===,不做类型转换,类型不同的一定不等。
==类型转换过程:
- 如果类型不同,进行类型转换
- 判断比较的是否是 null 或者是 undefined, 如果是, 返回 true .
- 判断两者类型是否为 string 和 number, 如果是, 将字符串转换成 number
- 判断其中一方是否为 boolean, 如果是, 将 boolean 转为 number 再进行判断
- 判断其中一方是否为 object 且另一方为 string、number 或者 symbol , 如果是, 将 object 转为原始类型再进行判断
经典面试题:[] == ![] 为什么是true
转化步骤:
-
!运算符优先级最高,
![]会被转为为false,因此表达式变成了:[] == false
14、如何实现sleep的效果(es5或者es6)
(1)通过promise来实现
function sleep(ms){
var temple=new Promise(
(resolve)=>{
console.log(111);setTimeout(resolve,ms)
});
return temple
}
sleep(500).then(function(){
//console.log(222)
})
//先输出了111,延迟500ms后输出222
(2)通过async封装
function sleep(ms){
return new Promise((resolve)=>setTimeout(resolve,ms));
}
async function test(){
var temple=await sleep(1000);
console.log(1111)
return temple
}
test();
//延迟1000ms输出了1111
15、箭头函数和普通函数有啥区别?箭头函数能当构造函数吗?
- 普通函数通过 function 关键字定义, this 无法结合词法作用域使用,在运行时绑定,只取决于函数的调用方式,在哪里被调用,调用位置。(取决于调用者,和是否独立运行)
- 箭头函数使用被称为 “胖箭头” 的操作
=>定义,箭头函数不应用普通函数 this 绑定的四种规则,而是根据外层(函数或全局)的作用域来决定 this,且箭头函数的绑定无法被修改(new 也不行)。- 箭头函数常用于回调函数中,包括事件处理器或定时器
- 箭头函数和 var self = this,都试图取代传统的 this 运行机制,将 this 的绑定拉回到词法作用域
- 没有原型、没有 this、没有 super,没有 arguments,没有 new.target
- 不能通过 new 关键字调用
16、深拷贝与浅拷贝
在项目中有许多地方需要数据克隆,特别是引用类型对象,我们无法使用普通的赋值方式克隆,虽然我们一般使用第三方库如lodash来实现深拷贝,但是我们也需要知道一些其中的原理
浅拷贝
Object.assign({},obj)浅拷贝objectobj1={...obj2}通过spread展开运算符浅拷贝obj2Object.fromEntries(Object.entries(obj))通过生成迭代器再通过迭代器生成对象Object.create({},Object.getOwnPropertyDescriptors(obj))浅拷贝objObject.defineProperties({},Object.getOwnPropertyDescriptors(obj))浅拷贝obj
深拷贝
JSON.parse(JSON.stringify(obj))通过JSON的2次转换深拷贝obj,不过无法拷贝undefined与symbol属性,无法拷贝循环引用对象- 自己实现深拷贝
简单深拷贝
//简单版深拷贝,只能拷贝基本原始类型和普通对象与数组,无法拷贝循环引用
function simpleDeepClone(a) {
const b=Array.isArray(a) ? [] : {}
for (const key of Object.keys(a)) {
const type = typeof a[key]
if (type !== 'object' || a[key] === null) {
b[key] = a[key]
} else {
b[key] = simpleDeepClone(a[key])
}
}
return b
}
17、script标签之async与defer
使用async属性
- 如果script标签设置了这个值,则说明引入的js需要异步加载和执行,注意此属性只适用于外部引入的js
- 在有async的情况下脚本异步加载和执行,并且不会阻塞页面加载,但是也并不会保证其加载的顺序,如果多个async优先执行,则先加载好的js文件,所以使用此方式加载的js文件最好不要包含其他依赖
使用defer属性
- 如果使用此属性,也将会使js异步加载执行,且会在文档被解析完成后执行,这样就不会阻塞页面加载,但是它将会按照原来的执行顺序执行,对于有依赖关系的也可使用
- html4.0中定义了defer,html5.0中定义了async
不同情况
-
如果只有async,那么脚本在下载完成后异步执行。
-
如果只有defer,那么脚本会在页面解析完毕之后执行。
-
如果都没有,那么脚本会在页面中马上解执行,停止文档解析阻塞页面加载
-
如果都有那么同async,当然此情况一般用于html的版本兼容下,如果没有async则defer生效
-
不过还是推荐直接把script标签放在body底部
18、谈谈你对 Promise 的理解? 和 ajax 有关系么?
Promise和ajax没有半毛钱直接关系.promise只是为了解决"回调地狱"而诞生的;
平时结合 ajax是为了更好的梳理和控制流程,这里我们简单梳理下..
Promise有三种状态,Pending/resolve()/reject();
一些需要注意的小点,如下
-
在
Pending转为另外两种之一的状态时候,状态不可在改变.. -
Promise的then为异步.而(new Promise())构造函数内为同步 -
Promise的catch不能捕获任意情况的错误(比如then里面的setTimout内手动抛出一个Error) -
Promise的then返回Promise.reject()会中断链式调用 -
Promise的resolve若是传入值而非函数,会发生值穿透的现象 -
Promise的catch还是then,return的都是一个新的Promise(在 Promise 没有被中断的情况下)Promise.resolve(1) .then(2) .then(Promise.resolve(3)) .then(console.log)
运行结果: 1
解释:.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透。
Promise.resolve(1)
.then(function(){return 2})
.then(Promise.resolve(3))
.then(console.log)
结果为2
Promise.resolve(1)
.then(function(){return 2})
.then(function(){return Promise.resolve(3)})
.then(console.log)
结果为3
1.Promise的立即执行性
var p = new Promise(function(resolve, reject){
console.log("create a promise");
resolve("success");
});
console.log("after new Promise");
p.then(function(value){
console.log(value);
});
控制台输出:
"create a promise"
"after new Promise"
"success"
2.Promise 状态的不可逆性
var p1 = new Promise(function(resolve, reject){
resolve("success1");
resolve("success2");
});
var p2 = new Promise(function(resolve, reject){
resolve("success");
reject("reject");
});
p1.then(function(value){
console.log(value);
});
p2.then(function(value){
console.log(value);
});
控制台输出:
"success1"
"success"
3.链式调用
var p = new Promise(function(resolve, reject){
resolve(1);
});
p.then(function(value){ //第一个then
console.log(value);
return value*2;
}).then(function(value){ //第二个then
console.log(value);
}).then(function(value){ //第三个then
console.log(value);
return Promise.resolve('resolve');
}).then(function(value){ //第四个then
console.log(value);
return Promise.reject('reject');
}).then(function(value){ //第五个then
console.log('resolve: '+ value);
}, function(err){
console.log('reject: ' + err);
})
控制台输出:
1
2
undefined
"resolve"
"reject: reject"
4.Promise then() 回调异步性
var p = new Promise(function(resolve, reject){
resolve("success");
});
p.then(function(value){
console.log(value);
});
console.log("which one is called first ?");
复制代码
控制台输出:
"which one is called first ?"
"success"
5.Promise 中的异常
var p1 = new Promise( function(resolve,reject){
foo.bar();
resolve( 1 );
});
p1.then(
function(value){
console.log('p1 then value: ' + value);
},
function(err){
//p1 then err: ReferenceError: foo is not defined //输出1:
console.log('p1 then err: ' + err);
}
).then(
function(value){
console.log('p1 then then value: '+value);
},
function(err){
//p1 then then value: undefined //输出3:
console.log('p1 then then err: ' + err);
}
);
var p2 = new Promise(function(resolve,reject){
resolve( 2 );
});
p2.then(
function(value){
//p2 then value: 2 //输出2:
console.log('p2 then value: ' + value);
foo.bar();
},
function(err){
console.log('p2 then err: ' + err);
}
).then(
function(value){
console.log('p2 then then value: ' + value);
},
function(err){
//p2 then then err: ReferenceError: foo is not defined //输出4:
console.log('p2 then then err: ' + err);
return 1;
}
).then(
function(value){
//p2 then then then value: 1 //输出5:
console.log('p2 then then then value: ' + value);
},
function(err){
console.log('p2 then then then err: ' + err);
}
);
控制台输出:
p1 then err: ReferenceError: foo is not defined
p2 then value: 2
p1 then then value: undefined
p2 then then err: ReferenceError: foo is not defined
p2 then then then value: 1
6.Promise.resolve()
var p1 = Promise.resolve( 1 );
var p2 = Promise.resolve( p1 );
var p3 = new Promise(function(resolve, reject){
resolve(1);
});
var p4 = new Promise(function(resolve, reject){
resolve(p1);
});
p4.then(function(value){
console.log('p4=' + value);
});
p2.then(function(value){
console.log('p2=' + value);
})
p1.then(function(value){
console.log('p1=' + value);
})
控制台输出:
p2=1
p1=1
p4=1
19、Promise是什么,可以手写实现一下吗?
解释: Promise方法链通过return传值,没有return就只是相互独立的任务而已
Promise,翻译过来是承诺,承诺它过一段时间会给你一个结果。从编程讲Promise 是异步编程的一种解决方案。下面是Promise在MDN的相关说明:
Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象。
一个 Promise有以下几种状态:
- pending: 初始状态,既不是成功,也不是失败状态。
- fulfilled: 意味着操作成功完成。
- rejected: 意味着操作失败。
这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,也就是说一旦状态变为 fulfilled/rejected 后,就不能再次改变。
其实Promise也是存在一些缺点的,比如无法取消 Promise,错误需要通过回调函数捕获。
promise手写实现,面试够用版:
function myPromise(constructor){
let self=this;
self.status="pending" //定义状态改变前的初始状态
self.value=undefined;//定义状态为resolved的时候的状态
self.reason=undefined;//定义状态为rejected的时候的状态
function resolve(value){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.value=value;
self.status="resolved";
}
}
function reject(reason){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.reason=reason;
self.status="rejected";
}
}
//捕获构造异常
try{
constructor(resolve,reject);
}catch(e){
reject(e);
}
}
// 定义链式调用的then方法
myPromise.prototype.then=function(onFullfilled,onRejected){
let self=this;
switch(self.status){
case "resolved":
onFullfilled(self.value);
break;
case "rejected":
onRejected(self.reason);
break;
default:
}
}
关于Promise还有其他的知识,比如Promise.all()、Promise.race()等的运用,由于篇幅原因就不再做展开
20、DOM 操作——怎样添加、移除、移动、复制、创建和查找节点?
(1)创建新节点
createDocumentFragment() //创建一个DOM片段
createElement() //创建一个具体的元素
createTextNode() //创建一个文本节点
(2)添加、移除、替换、插入
appendChild(node)
removeChild(node)
replaceChild(new,old)
insertBefore(new,old)
(3)查找
getElementById();
getElementsByName();
getElementsByTagName();
getElementsByClassName();
querySelector();
querySelectorAll();
(4)属性操作
getAttribute(key);
setAttribute(key, value);
hasAttribute(key);
removeAttribute(key);
21、谈谈JS的运行机制
1. js单线程
JavaScript语言的一大特点就是单线程,即同一时间只能做一件事情。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
2. js事件循环
js代码执行过程中会有很多任务,这些任务总的分成两类:
- 同步任务
- 异步任务
当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。,我们用导图来说明: 我们解释一下这张图:
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入
Event Table并注册函数。 - 当指定的事情完成时,
Event Table会将这个函数移入Event Queue。 - 主线程内的任务执行完毕为空,会去
Event Queue读取对应的函数,进入主线程执行。 - 上述过程会不断重复,也就是常说的
Event Loop(事件循环)。
那主线程执行栈何时为空呢?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。
以上就是js运行的整体流程
需要注意的是除了同步任务和异步任务,任务还可以更加细分为macrotask(宏任务)和microtask(微任务),js引擎会优先执行微任务
微任务包括了
promise 的回调、
node 中的 process.nextTick 、
对 Dom 变化监听的 MutationObserver。
宏任务包括了
script 脚本的执行、
setTimeout ,setInterval ,setImmediate 一类的定时事件,
还有如 I/O 操作、
UI 渲染等。
面试中该如何回答呢? 下面是我个人推荐的回答:
- 首先js 是单线程运行的,在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。
- 在执行同步代码的时候,如果遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务
- 当同步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行。
- 任务队列可以分为宏任务对列和微任务对列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。
- 当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。
3、最后可以用下面一道题检测一下收获:
setTimeout(function() {
console.log(1)
}, 0);
new Promise(function(resolve, reject) {
console.log(2);
resolve()
}).then(function() {
console.log(3)
});
process.nextTick(function () {
console.log(4)
})
console.log(5)
第一轮:主线程开始执行,遇到setTimeout,将setTimeout的回调函数丢到宏任务队列中,在往下执行new Promise立即执行,输出2,then的回调函数丢到微任务队列中,再继续执行,遇到process.nextTick,同样将回调函数扔到为任务队列,再继续执行,输出5,当所有同步任务执行完成后看有没有可以执行的微任务,发现有then函数和nextTick两个微任务,先执行哪个呢?process.nextTick指定的异步任务总是发生在所有异步任务之前,因此先执行process.nextTick输出4然后执行then函数输出3,第一轮执行结束。 第二轮:从宏任务队列开始,发现setTimeout回调,输出1执行完毕,因此结果是25431
22、模块化
模块化开发在现代开发中已是必不可少的一部分,它大大提高了项目的可维护、可拓展和可协作性。通常,我们 在浏览器中使用 ES6 的模块化支持,在 Node 中使用 commonjs 的模块化支持。
1、分类:
- es6:
import / export - commonjs:
require / module.exports / exports - amd:
require / defined
2、require与import的区别
require支持 动态导入,import不支持require是 同步 导入,import属于 异步 导入require是 值拷贝,导出值变化不会影响导入值;import指向 内存地址,导入值会随导出值而变化
3、requireJS的核心原理是什么?
require.js 的核心原理是通过动态创建 script 脚本来异步引入模块,然后对每个脚本的 load 事件进行监听,如果每个脚本都加载完成了,再调用回调函数。```
4、js 的几种模块规范?
js 中现在比较成熟的有四种模块加载方案:
-
第一种是 CommonJS 方案,它通过 require 来引入模块,通过 module.exports 定义模块的输出接口。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题。但如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。
-
第二种是 AMD 方案,这种方案采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。require.js 实现了 AMD 规范。
-
第三种是 CMD 方案,这种方案和 AMD 方案都是为了解决异步模块加载的问题,sea.js 实现了 CMD 规范。它和require.js的区别在于模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同。
-
第四种方案是 ES6 提出的方案,使用 import 和 export 的形式来导入导出模块。
25、TypeScript
TypeScript的特点
-
类型检查。TypeScript在编译代码时进行严格的静态类型检查。这就意味着在编码阶段可能存在的隐患,不必把它们带到线上。
-
语言扩展。TypeScript会包括ES6和未来提案中的特性,比如异步操作和装饰器。也会从其他语言借鉴某些特点,比如接口和抽象类。
-
工具属性。TypeScript 可以编译成标准的 Javascript。可以在任何浏览器和操作系统上运行。无需任何运行时额外开销。从这个角度讲TypeScript更像时一个工具。
类型
TypeScript中定义了布尔值boolean、数字number、字符串string、数组Array、元组、枚举、接口、any、void等。
-
元组。元组表示的是一个已知元素数量和类型的数组,各元素的类型不必相同。
let x: [string, number, boolean] = ['hello', 100, true];
-
接口。interface 来定义一个接口,接口可以理解成描述了一种数据类型。比如我们定义了一个方法要接受的参数,这个参数必须有哪些属性或者方法。这个时候就可以使用接口定义方法的参数了。
interface params{ search: string; page?: number; size?: number; }
function foo(p: params): string{}
-
泛型。泛型保证了类型的非确定性和一致性。比如在函数中,我们为了保证函数返回值类型和输入变量类型一致,我们可以使用泛型。
模块和命名空间
-
TypeScript 中模块的用法和 ES6 的 Module 保持一致。使用 export 语法导出模块的变量和方法;使用 import 引入其他模块的变量和方法。
-
命名空间。TypeScript 中使用 namespace 关键字来实现命名空间。比如在 Shape 命名空间下的变量只有在该命名空间下可以访问,如果要在全局访问的变量和方法,要通过 export 关键字导出。
-
命名空间可以拆分为几个文件。
-
命名空间最终被编译为一个全局变量和一个立即执行函数。
最后, 命名空间不要和模块混用 ,同时命名空间的使用最好在一个全局的环境中使用
声明文件
如果我们想在 TypeScript 中使用第三方类库如 jQuery、lodash等,需要提供这些类库的声明文件(以.d.ts结尾),对外暴露API。一般我们通过安装第三方类库的类型声明包后,即可在 TypeScript 中使用。以 jQuery 为例:
npm install -D jquery @types/jquery
TS 编译流程
和Babel以及其他编译到 Javascript 的工具类似, TS 的编译流程包含一下三步:
解析 -> 转换 -> 生成