JS
1. 知道哪些ES6语法,怎么用
举例回答:
-
let : 声明变量。只在它的块级作用域内有效;没有变量提升,必须先声明后使用;不能重复声明。
-
const : 声明一个只读的常量。一旦声明,常量的值就不能改变。只在声明所在的块级作用域内有效;一旦声明变量,就必须立即初始化,不能留到以后赋值;不提升;不可重复声明。
-
解构赋值:数组的解构:
let [a,b,c]=[1,2,3]
从数组中取出值,根据对应的位置赋给变量。 对象的解构:let a=obj.a
等价于let {a}=obj
-
...
扩展运算符:接一个数组,把数组变成一个参数序列 -
class:类关键字
-
模块功能export import:export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。
分为两种:具名导出:
export {fn1,fn2}
导入:import {fn1,fn2} from 'xxx.js'
导入的必须和导出的同名 默认导出:export default fn1
导入:import f from 'xxx.js'
可以任意名称 -
箭头函数:写起来简短,没有this
-
默认参数:函数默认参数允许在没有值或undefined被传入时使用默认形参。
2. Promise,Promise.all,Promise.race 怎么用
Promise是异步编程的一种解决方案,比传统的回调函数要更方便,合理,避免了回调地狱。
Promise对象只有三种状态:进行中,已成功,已失败。一个Promise对象代表了一个异步操作。这个状态只能改变一次。进行中->成功,进行中->失败。
用法:new Promise创建一个promise实例,Promsie构造函数接受一个函数作为参数。这个函数的两个参数分别是resolve和reject,它们也是函数。resolve函数会在异步操作成功后被调用,把异步操作的结果作为参数传递出去;reject函数会在异步操作失败后被调用,把错误的信息作为参数传递出去。
创建了promise实例后,通过then方法给状态变化时添加回调函数。then接受两个回调函数作为参数,第一个是状态变为resolve时被调用,第二个状态变为reject被调用。
Promise.all: 接收多个promise实例作为参数,包装成一个新的promise实例const p = Promise.all([p1, p2, p3]);
p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
Promise.race: 同样是将多个 Promise 实例,包装成一个新的 Promise 实例.const p = Promise.race([p1, p2, p3]);
只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。
Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
3. 手写函数防抖和函数节流
在时间轴上控制函数的执行次数。
-
函数防抖:在事件被触发n秒后,再执行它的回调函数。如果在这n秒的期间又被触发了,就再次重新计时。总的来说,适合多次事件一次响应的情况
任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行。
就像进电梯,第一个人进电梯(触发事件),电梯不会马上运行,而是等10秒钟,如果这10秒内又有人上来了(又被触发了),就重新计时10秒钟。直到10秒钟内没人再上来了,电梯就运行了。
function debounce(fn,delay){ let timer=null return function(){ let context=this if(timer){ window.clearTimeout(timer) } timer=setTimeout(()=>{ fn.apply(context,arguments) timer=null },delay) } } const debounced=debounce(()=>{console.log('hi')}) debounced() debounced()
关于为什么fn要用apply调用:因为可能回调函数fn需要参数,或者需要指定this。这些信息只能通过debounced 函数传进去,也就是debounce里return的那个匿名函数。所以需要保持fn被调用的时候,它里边的this和参数与debounced传进去的一致,才能将这些信息给到fn。如果不用apply,而是直接
fn()
去调用,那么fn里的this就是window了。所以需要用apply,去指定fn里边的this和参数。如果setTimeout里用的是箭头函数,在return的函数里,不将this赋给context也行,也就是这句
let context=this
可以删掉。直接fn.apply(this,arguments)
就可以。因为setTimeout里用的是箭头函数,所以里边的this和外边的一样,也就是和传给debounced的this一样。但是如果setTimeout里用的是匿名函数,就需要先保存this了。原因:关于setTimeout的this指向
-
函数节流:理解为冷却时间,一次技能发出后,必须间隔一段时间才能再次发出。怎么实现?利用闭包,为true就可以执行,执行后置为false,然后规定,几秒钟后再把该标记置为true
function throttle(fn,delay){ let canuse=true return function(){ if(canuse){ fn.apply(this,arguments) canuse=false setTimeout(()=>{ canuse=true },delay) } } } const throttled=throttle(()=>{console.log('hi')}) throttled() throttled()
一段时间执行一次之后,就不执行第二次
4. 手写AJAX
-
背代码,完整版
const request=new XMLHttpRequest() request.open('GET','/xxx') request.onreadystatechange=()=>{ if(request.readyState===4){ console.log('下载完成') if(request.status>=200 && request.status<300){ console.log('请求成功') }else{ console.log('请求失败') } } } request.send()
-
背代码,简化版
var request = new XMLHttpRequest() request.open('GET', '/a/b/c?name=ff', true) request.onload = ()=> console.log(request.responseText) request.send()
5. 这段代码里的this是什么
-
背代码
- fn()
this => window/global - obj.fn()
this => obj - fn.call(xx)
this => xx - fn.apply(xx)
this => xx - fn.bind(xx)
this => xx - new Fn()
this => 新的对象 - fn = ()=> {}
this => 这个函数外面的 this
- fn()
6.1 什么是闭包
- 要点:阐述什么是闭包,闭包的作用:
一个函数内部使用了外部的变量,那么这个函数和变量就构成了一个闭包。
闭包的作用:隐藏一个变量,间接访问一个变量。
function foo(){
var local = 1
function bar(){
local++
return local
}
return bar
}
var func = foo()
func()
比如这样,一个函数嵌套一个函数,return这个函数。调用外边的函数就会得到一个接口,用这个接口访问局部变量。local和bar就构成了一个闭包。
通过使用闭包,我们可以实现在一个函数外边访问到它的内部变量。通常这是一个父函数,我们想在父函数外部访问它的局部变量,可以在它的内部声明一个子函数,这个子函数是可以使用父函数的内部变量的,所以我们return 这个子函数。在外边通过子函数这个接口访问父函数的内部变量。
闭包还会让这些局部变量的值始终保存在内存里。比如foo调用完以后,local并不会被回收。因为子函数bar被引用给了一个全局变量,所以bar要一直存在,而bar的存在又依赖它的父函数,所以foo函数也会一直保存在内存里。
闭包的缺点:闭包会使函数里的变量一直在内存里,导致内存消耗很大。
6.2 什么是立即执行函数,有什么作用
js立即执行函数可以让你的函数在创建后立即执行,js立即执行函数模式是一种语法,可以让你的函数在定义后立即被执行,这种模式本质上就是函数表达式(命名的或者匿名的),在创建后立即执行。
但是这样写浏览器会报错,为了通过检查,我们一般有以下几种写法:
(function(){alert('我是匿名函数')} ()) // 用括号把整个表达式包起来
(function(){alert('我是匿名函数')}) () //用括号把函数包起来
!function(){alert('我是匿名函数')}() // 求反,我们不在意值是多少,只想通过语法检查。
作用:创建一个独立的作用域,使外边访问不到这个作用域里的内部变量,避免变量污染。
7. 什么是跨域,CORS,JSONP
浏览器出于安全方面的考虑,有一个叫做同源策略的规定,它规定,不同源的页面之间不允许互相访问数据。但是现实情况我们需要访问不同源的资源,跨域就是我们通过一些手段实现访问不同源的资源。
CORS和JSONP是实现跨域的两种方法。
CORS是跨域资源共享,比如某一个源想把自己的数据公开给谁,就在后台服务器添加一个响应头:
response.setHeader("Access-Control-Allow-Origin", "http://anqi.com:9999");
后面可以指定允许谁访问。
我们在跨域的时候,由于当前浏览器不支持CORS,所以就需要另外一种方法来跨域。我们通过创建一个script标签,请求另外一个网站的一个JS文件,这个JS文件里通过执行一个回调,来获取我们想得到的数据。这个回调函数的名字是我们随机生成的,是一个随机数,我们常常以callback这个参数把函数名传到后台,后台得到这个名字再传给我们,然后执行这个函数。
JSONP的优点,兼容 IE,可以跨域。 缺点:由于JSONP是用script标签引用JS的,所以没有AJAX那么精确,它读不到状态码,也不知道响应头,只知道执行成功还是失败。并且,由于是script标签,只能发GET请求,不支持POST。
8. async和await怎么用,如何捕获异常?
async是什么?与promise有关;让异步函数更像是同步函数。
为什么要用await?和promise.then.then相比,用await可以让异步代码看起来更像是标准的同步代码。从上到下执行,但是promise代码的执行顺序不能直接看懂。
function resolveAfter2Seconds() {
return new Promise(resolve => {
setTimeout(() => {
resolve('resolved');
}, 2000);
});
}
async function asyncCall() {
console.log('calling');
const result = await resolveAfter2Seconds();
console.log(result);
// expected output: "resolved"
}
asyncCall();
使用async function 声明一个异步函数,这个异步函数返回一个Promise对象。await关键字只能用在async函数里,await后边接一个返回Promise的函数,并且调用这个函数。await会暂停当前的异步函数,等待Promise执行完毕。await 关键字只在异步函数内有效
用try catch捕获异常,把await放在try里
参考文章:async/await 和 promise 的执行顺序
9. 如何实现深拷贝?
-
背代码,要点:
- 递归
- 判断类型
- 检查循环引用(也叫环)
- 需要忽略原型
前置知识:浅拷贝如果是复杂数据类型,只会拷贝地址,还是共用同一块内存。所以改变原对象也会影响拷贝的对象。深拷贝是从内存完整的拷贝一份,没有共用内存地址,两个对象互不影响。
实现深拷贝最简陋的做法就是JSON.parse(JSON.stringify(source))
但是无法涵盖数组,循环引用的情况。
基本的做法:先创建一个空对象作为结果。对于要拷贝的参数,首先判断它的类型,如果是对象,就要递归调用,把深层次的属性依次拷贝。如果是基本数据类型,就直接返回。
但是如果是数组,就不能拷贝成对象的形式。需要在初始化时判断如果要拷贝的是一个数组类型,就初始化一个空数组,其他和对象一样。
循环引用:如果这个对象的一个属性对应的value是它自己,obj.a=obj
递归就会进入死循环。解决办法:需要一块存储空间保存新对象和旧对象的关系。在拷贝对象的每一个属性的时候,先去这个空间里找,这个对象有没有被拷贝过,如果已经被拷贝过了(引用自己的情况),那就结束本地递归,直接返回上次被拷贝过了的结果值;如果这个对象还没被拷贝过,是一个新的属性,就正常往下走,还要把当前这个对象和它的拷贝对象存进空间里。这个存储空间用map对象,因为map也是以key-value存储,而且key可以是任意类型。
function deepClone(source,map=new Map()){
if(typeof source==='object'){
let result=Array.isArray(source) ? []:{}
if(map.get(source)){
return map.get(source)
}
map.set(source,result)
for(let key in source){
result[key]=deepClone(source[key],map)
}
return result
}else{
return source
}
}
参考文章:如何写出一个惊艳面试官的深拷贝?
10. js判断数据类型的三种方法
-
typeof
返回一个字符串。问题:无法正确返回复杂数据类型的类型。都是
'object'
-
instanceof
a instanceof A 判断a是不是A的实例,返回true/false
它可以判断出数组,函数,日期等引用类型
[] instanceof Array; // true {} instanceof Object;// true newDate() instanceof Date;// true function Person(){}; new Person() instanceof Person; //true
-
Object.prototype.toString.call
用对象原型上的
toString
方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。
Object.prototype.toString.call('') ; // [object String] Object.prototype.toString.call(1) ; // [object Number] Object.prototype.toString.call(true) ; // [object Boolean] Object.prototype.toString.call(Symbol()); //[object Symbol] Object.prototype.toString.call(undefined) ; // [object Undefined] Object.prototype.toString.call(null) ; // [object Null] Object.prototype.toString.call(new Function()) ; // [object Function] Object.prototype.toString.call(new Date()) ; // [object Date] Object.prototype.toString.call([]) ; // [object Array] Object.prototype.toString.call(new RegExp()) ; // [object RegExp] Object.prototype.toString.call(new Error()) ; // [object Error] Object.prototype.toString.call(document) ; // [object HTMLDocument] Object.prototype.toString.call(window) ; //[object global] window 是全局对象 global 的引用
11.1 用class实现继承
class Animal{
constructor(type){
this.type=type
}
move(){
console.log('跑起来')
}
}
class Dog extends Animal{
constructor(type,name){
super(type)//继承里面必须super
this.name=name
}
eat(){}
}
11.2 不用class实现继承
function Animal(type){
this.type=type
}
Animal.prototype.move=function(){}
function Dog(type,name){
Animal.call(this,type)//调用父类函数
this.name=name//自己的属性
}
// 实现原型的继承,即Dog.prototype.__proto__=Animal.prototype
let f=function(){}
f.prototype=Animal.prototype
Dog.prototype=new f()
//constructor变成Dog
Dog.prototype.constructor=Dog
//自己新的属性可以自己写
Dog.prototype.eat=function(){}
const dog=new Dog('dog','hi')
12. 用正则实现 trim()
去掉字符串开头和结尾的空白字符。
// /正则表达式/ 包含在斜杠之间就是正则表达式
// ^ 匹配输入字符串开始的位置
// \s 匹配空字符串,包括空格、制表符、换页符等
// * 或 + 匹配零次或多次
// | 或者
// $ 匹配输入字符串结束的位置
// g 全局搜索
function trim(string){
return string.replace(/^\s+|\s+$/g,'')
}
如果不加g,只会去掉开头的空格,结尾的没去掉。
13. 如何解决异步回调地狱
promise、generator、async/await
14. 图片的懒加载和预加载
预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染。
懒加载:懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数。
两种技术的本质:
两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。
懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。
15. js的new操作符做了哪些事情
new 操作符新建了一个空对象,这个对象原型指向构造函数的prototype,执行构造函数后返回这个对象。
16. 函数柯里化
什么是函数柯里化?把接受多个参数的函数,转换成一个接受单一参数的函数
就是函数里return 另一个函数
function sum(x){
return function(y){
return x+y
}
}
sum(1)(2) //3
普通方法:
function sum(x,y){
return x+y
}
也就是说,原来实现两个数相加,要在一个括号里传两个参数。
利用函数柯里化,每次调用只传一个参数,返回一个匿名函数,再传一个参数调用,就会得到最后的结果。
三个数相加也同理:
function sum(x){
return function(y){
return function(z){
return x+y+z
}
}
}
console.log(sum(1)(2)(3)) //6
17. null和undefined区别
null表示"没有对象",即该处不应该有值。
典型用法是:
(1) 作为函数的参数,表示该函数的参数不是对象。
(2) 作为对象原型链的终点。
Object.getPrototypeOf(Object.prototype)
// null
undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。
典型用法是:
(1)变量被声明了,但没有赋值时,就等于undefined。
(2)调用函数时,应该提供的参数没有提供,该参数等于undefined。
(3)对象没有赋值的属性,该属性的值为undefined。
(4)函数没有返回值时,默认返回undefined。
var i;
i // undefined
function f(x){console.log(x)}
f() // undefined
var o = new Object();
o.p // undefined
var x = f();
x // undefined
18. require和import的区别
require属于CommonJS模块规范,而import属于ES6模块规范。
区别主要有两点:
- require加载,只能在运行时加载,运行时在能确定模块的依赖关系,输入输出等。因为它加载的是一个对象,这个对象只有在脚本执行时才能得到。 而import可以在编译时就完成加载,因为它是静态导入,不是对象
- require加载得到的值是只是这个值的拷贝。加载完以后,原来模块里这个值的变化,反应不到外边。就算原来模块里的值变了,加载得到的也还是一开始的那个值。 而import导入的值是引用。导入的这个变量,会随着原来模块里的变量的变化一起变化。如果原来模块里的变了,那在外边读到的值也是新的值。
19. 什么是按需加载
当用户触发了动作时才加载对应的功能。触发的动作,是要看具体的业务场景而言,包括但不限于以下几个情况:鼠标点击、输入文字、拉动滚动条,鼠标移动、窗口大小更改等。加载的文件,可以是JS、图片、CSS、HTML等。