天天看点

【直接收藏】前端JavaScript面试100问(中)

作者:陆荣涛

31、http 的理解 ?

HTTP 协议是超文本传输协议,是客户端浏览器或其他程序“请求”与 Web 服务器响应之间的应用层通信协议。

HTTPS主要是由HTTP+SSL构建的可进行加密传输、身份认证的一种安全通信通道。

32、http 和 https 的区别 ?

  • 1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
  • 2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
  • 3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
  • 4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

33、git 的常用指令有哪些 ?

git branch 分支查看

git branch branch_1 增加分支

git checkout branch 分支切换

git merge branch_1 合并分支(合并前要切换当前分支至master)

git branch -d branch_1 删除分支

git remote 查看当前仓库管理的远程仓库信息

git remote show origin 查看指定的远程仓库的详细信息

git push --set-upstream origin branch_1 第一次将本地分支推到远程仓库

git push <远程主机名> <本地分支名>:<远程分支名> 将本地分支推到远程分支

git pull <远程主机名> <远程分支>:<本地分支> 将远程分支拉到本地分支

git branch -d branch_0 删除本地合并后分支

git brench -D branch_0 删除本地未合并分支

it push origin --delete branch_0 删除远程分支

git restore [filename] 进行清除工作区的改变

git tag 查看标签

git tag v1.0.0 打标签

git push origin v1.0.0 将tag同步到远程服务器

34、平时是使用 git 指令还是图形化工具 ?

repository:git库相关操作,基本意思就是字面意思。

  • 1)资源管理器中浏览该Git库工作空间文件,省去查找路径不断点击鼠标的操作。
  • 2)启动Git bash工具(命令行工具)。
  • 3)查看当前分支文件状态,不包括未提交的信息。
  • 4)查看某个分支的文件(弹出框中可选择需要查看的版本、分支或标签),跟上一条差不多,用的比较少,可能是没有这方面的额需求。
  • 5)可视化当前分支历史、可视化所有分支历史:弹出分支操作历史,也就是gitk工具,放到gitk工具中介绍。
  • edit:用于操作commit时操作信息输入,只能操作文字输入部分,你没有看错。常用的快捷键大家都知道,何必要单独做成基本没啥用的。本来以为对变更的文件进行批量操作、本来以为可以对未版本跟踪的文件批量删除、本来、、、,都说了是本来。
  • Branch:新建分支(需要选择其实版本,可以根据版本号、其他分支或标签来选择)、检出分支(觉得切换分支更合适)、重命名分支、删除分支、当前分支Reset操作(会丢弃所有未提交的变更,包括工作区和索引区,当然了,有弹出框提示危险操作)。

35、Promsie.all() 使用过吗, 它是怎么使用的 ?

promise.all()用于一个异步操作需要在几个异步操作完成后再进行时使用。

promise.all()接受一个promise对象组成的数组参数,返回promise对象。

当数组中所有promise都完成了,就执行当前promise对象的then方法,如果数组中有一个promise执行失败了,就执行当前promise对象的catch方法。

36、什么是三次握手和四次挥手 ?

三次握手是网络客户端跟网络服务器之间建立连接,并进行通信的过程。相当于客户端和服务器之间你来我往的3个步骤。

  • 第一次握手是建立连接,客户端发送连接请求报文,并传送规定的数据包;
  • 第二次握手是服务器端表示接收到连接请求报文,并回传规定的数据包;
  • 第三次握手是客户端接收到服务器回传的数据包后,给服务器端再次发送数据包。这样就完成了客户端跟服务器的连接和数据传送。

四次挥手表示当前这次连接请求已经结束,要断开这次连接。

  • 第一次挥手是客户端对服务器发起断开请求,
  • 第二次握手是服务器表示收到这次断开请求,
  • 第三次握手是服务器表示已经断开连接
  • 第四次握手是客户端断开连接。

37、for in 和 for of 循环的区别 ?

`for in` 用于遍历对象的键(`key`),`for in`会遍历所有自身的和原型链上的可枚举属性。如果是数组,for in会将数组的索引(index)当做对象的key来遍历,其他的object也是一样的。 `for of`是`es6`引入的语法,用于遍历 所有迭代器iterator,其中包括`HTMLCollection`,`NodeList`,`Array`,`Map`,`Set`,`String`,`TypedArray`,`arguments`等对象的值(`item`)。

38、async/await 怎么抛出错误异常 ?

如果可能出错的代码比较少的时候可以使用try/catch结构来了处理,如果可能出错的代码比较多的时候,可以利用async函数返回一个promise对象的原理来处理,给async修饰的函数调用后返回的promise对象,调用catch方法来处理异常。

39、 函数式编程和命令式编程的区别 ?

  • 命令式编程(过程式编程) :

专注于”如何去做”,这样不管”做什么”,都会按照你的命令去做。解决某一问题的具体算法实现。

  • 函数式编程:把运算过程尽量写成一系列嵌套的函数调用。

函数式编程强调没有”副作用”,意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。

所谓”副作用”,指的是函数内部与外部交互(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。

40、http 常见的响应状态码 ?

100——客户必须继续发出请求

101——客户要求服务器根据请求转换HTTP协议版本

200——交易成功

201——提示知道新文件的URL

202——接受和处理、但处理未完成

203——返回信息不确定或不完整

204——请求收到,但返回信息为空

205——服务器完成了请求,用户代理必须复位当前已经浏览过的文件

206——服务器已经完成了部分用户的GET请求

300——请求的资源可在多处得到

301——删除请求数据

302——在其他地址发现了请求数据

303——建议客户访问其他URL或访问方式

304——客户端已经执行了GET,但文件未变化

305——请求的资源必须从服务器指定的地址得到

306——前一版本HTTP中使用的代码,现行版本中不再使用

307——申明请求的资源临时性删除

400——错误请求,如语法错误

401——请求授权失败

402——保留有效ChargeTo头响应

403——请求不允许

404——没有发现文件、查询或URl

405——用户在Request-Line字段定义的方法不允许

406——根据用户发送的Accept拖,请求资源不可访问

407——类似401,用户必须首先在代理服务器上得到授权

408——客户端没有在用户指定的饿时间内完成请求

409——对当前资源状态,请求不能完成

410——服务器上不再有此资源且无进一步的参考地址

411——服务器拒绝用户定义的Content-Length属性请求

412——一个或多个请求头字段在当前请求中错误

413——请求的资源大于服务器允许的大小

414——请求的资源URL长于服务器允许的长度

415——请求资源不支持请求项目格式

416——请求中包含Range请求头字段,在当前请求资源范围内没有range指示值,请求也不包含If-Range请求头字段

417——服务器不满足请求Expect头字段指定的期望值,如果是代理服务器,可能是下一级服务器不能满足请求

500——服务器产生内部错误

501——服务器不支持请求的函数

502——服务器暂时不可用,有时是为了防止发生系统过载

503——服务器过载或暂停维修

504——关口过载,服务器使用另一个关口或服务来响应用户,等待时间设定值较长

505——服务器不支持或拒绝支请求头中指定的HTTP版本

41、 什么是事件流以及事件流的传播机制 ?

事件触发后,从开始找目标元素,然后执行目标元素的事件,再到离开目标元素的整个过程称之为事件流。

W3C标准浏览器事件流的传播分为3个阶段:捕获阶段、目标阶段、冒泡阶段

  • 捕获阶段指找目标元素的过程,这个找的过程,是从最大的document对象到html,再到body,。。。直到目标元素。
  • 找到目标元素后,调用执行他绑定事件时对应的处理函数,这个过程被称之为目标阶段。
  • 当目标元素的事件执行结束后,再从目标元素,到他的父元素。。。body、html再到document的过程,是冒泡阶段。

42、模块化语法 ? commonJS AMD CMD ES6 Module

  • commonJS是nodejs自带的一种模块化语法,将一个文件看做是一个模块,可以将文件中导出的时候,被另一个文件导入使用。导出使用:module.exports导出。导入使用:require函数导入。
  • AMD是社区开发的模块化语法,需要依赖require.js实现,分为定义模块,导出数据和导入模块,使用数据。AMD语法的导入是依赖前置的,也就是说,需要用到的文件需要在第一次打开页面全部加载完成,造成的后果就是首屏加载很慢,后续操作会很流畅。
  • CMD是玉伯开发的模块化语法,需要依赖sea.js实现,也分为模块定义导出,和模块导入使用数据。CMD语法可以依赖前置,也可以按需导入,缓解了AMD语法的依赖前置。
  • ES6的模块化语法,类似于commonJS的语法,分为数据导出和数据导入,导入导出更加灵活。

43、 什么是懒加载和预加载 ?

  • 懒加载:懒加载也叫延迟加载,延迟加载网络资源或符合某些条件时才加载资源。常见的就是图片延时加载。懒加载的意义:懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数。懒惰实现方式:
    • 1.第一种是纯粹的延迟加载,使用setTimeOut或setInterval进行加载延迟.
    • 2.第二种是条件加载,符合某些条件,或触发了某些事件才开始异步下载。
    • 3.第三种是可视区加载,即仅加载用户可以看到的区域,这个主要由监控滚动条来实现,一般会在距用户看到某图片前一定距离便开始加载,这样能保证用户拉下时正好能看到图片。
  • 预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染。

两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。预加载应用如广告弹窗等。

44、token 一般存放在哪里 ? 为什么不存放在 cookie 内 ?

token一般放在本地存储中。token的存在本身只关心请求的安全性,而不关心token本身的安全,因为token是服务器端生成的,可以理解为一种加密技术。但如果存在cookie内的话,浏览器的请求默认会自动在请求头中携带cookie,所以容易受到csrf攻击。

45、 less 和 sass 的区别 ?

  • 编译环境不一样,sass是服务器端处理的,可以用Ruby、node-sass来编译;less需要引入less.js来处理输出,也可以使用工具在服务器端处理成css,也有在线编译的。
  • 变量定义符不一样,less用的是@,而sass用$。
  • sass支持分支语句,less不支持

44、浏览器的同源策略机制 ?

同源策略,又称SOP,全称Same Origin Policy,是浏览器最基本的安全功能。站在浏览器的较短看网页,如果网络上的接口可以不受限制、无需授权随意被人调用,那将是一个非常严重的混乱场景。浏览器为了安全有序,内部实现了同源策略。

同源策略,指的是浏览器限制当前网页只能访问同源的接口资源。

所谓同源,指当前页面和请求的接口,两方必须是同协议、且同域名、且同端口。只要有一个不相同,则会受到浏览器约束,不允许请求。

但当一个项目变的很大的时候,将所有内容放在一个网站或一个服务器中会让网站变的臃肿且性能低下,所以,在一些场景中,我们需要跨过同源策略,请求到不同源的接口资源,这种场景叫跨域。

跨域大致有3种方案:

  • jsonp
  • 这种方式是利用浏览器不限制某些标签发送跨域请求,例如link、img、iframe、script。通常请求请求回来的资源要在js中进行处理,所以jsonp跨域是利用script标签进行发送,且这种请求方式只能是get请求。
  • cors
  • 这种方式是让接口资源方面进行授权,授权允许访问。在接口资源处添加响应头即可通过浏览器的同源策略,响应头具体的键值对如下:
  • {Access-Control-Allow-Origin: '*'}
  • proxy
  • 这种方式属于找外援的一种方式,浏览器只能限制当前正在打开的web页面发送请求,但无法限制服务器端请求接口资源。所以我们可以将请求发送到自己服务器,然后自己服务器去请求目标接口资源,最后自己服务器将接口资源返回给当前页面,类似于找外援代替自己请求目标接口资源。
  • 这种方式通常要对服务器进行代理配置,需要对apache服务器、nginx服务器、nodejs服务器进行配置。

45、 浏览器的缓存有哪些 ? 什么时候使用强制缓存 ? 什么时候使用协商缓存 ?

当我们访问同一个页面时,请求资源、数据都是需要一定的耗时,如果可以将一些资源缓存下来,那么从第二次访问开始,就可以减少加载时间,提高用户体验,也能减轻服务器的压力。

浏览器缓存分为强缓存和协商缓存,当存在缓存时,客户端第一次向服务器请求数据时,客户端会缓存到内存或者硬盘当中,当第二次获取相同的资源,强缓存和协商缓存的应对方式有所不同。

强缓存:当客户端第二次向服务器请求相同的资源时,不会向服务器发送请求,而是直接从内存/硬盘中间读取。缓存由服务器的响应头里 cache-control 和 expires 两个字段决定

协商缓存:当客户端第二次向服务器请求相同的资源时,先向服务器发送请求"询问"该请求的文件缓存在ben'd与服务器相比是否更改,如果更改,则更新文件,如果没有就从内存/硬盘中读取。协商缓存由 last-modified 和 etag两个字段决定

46、 数组方法 forEach 和 map 的区别 ?

forEach和map都是循环遍历数组中的每一项。forEach() 和 map() 里面每一次执行匿名函数都支持3个参数:数组中的当前项item,当前项的索引index,原始数组input。匿名函数中的this都是指Window。只能遍历数组。

他们的区别是:forEach没有返回值,但map中要有返回值,返回处理后的所有新元素组成的数组。

47、 什么是函数作用域 ? 什么是作用域链 ?

作用域就是在代码执行过程中,形成一个独立的空间,让空间内的变量不会泄露在空间外,也让独立空间内的变量函数在独立空间内运行,而不会影响到外部的环境。

作用域分为全局作用域和局部作用域,也就是本来有一个巨大的空间,空间内定义的函数内部,就形成了一个独立的小空间,全局作用域是最大的作用域。

但是当独立空间内的数据不能满足需求时,是可以从外部获取数据的,也就是说这样的独立空间之间是可以有层级关系的,外部的空间不可以从内部的空间获取数据,但内部的空间可以。当子级空间在父级空间中获取数据的时,父级空间没有的话,父级空间也会到他的父级空间中查找数据,这样形成的链式结构叫作用域链。

当将一个变量当做值使用时,会先在当前作用域中查找这个变量的定义和数据,如果没有定义的话,就会去父级作用域中查找,如果父级作用域中有的话就使用这个值,如果父级作用域中也没有的话,就通过父级作用域查找他的父级作用域,直到找到最大的作用域-全局,如果全局也没有就报错。

当将一个变量当做数据容器存储,也就是给变量赋值的时候,也要先在自己作用域中查找变量的定义,如果没有就在上一级作用域中查找,直到全局,如果全局作用域中也没有这个变量的定义,就在全局定义这个变量并赋值。

48、 ES6 中 Set 和 Map 的原理 ?

Set 是无重复值的有序列表。根据 `Object.is()`方法来判断其中的值不相等,以保证无重复。Set 会自动移除重复的值,因此你可以使用它来过滤数组中的重复值并返回结果。Set并不是数组的子类型,所以你无法随机访问其中的值。但你可以使用`has()` 方法来判断某个值是否存在于 Set 中,或通过 `size` 属性来查看其中有多少个值。Set 类型还拥有`forEach()`方法,用于处理每个值

Map 是有序的键值对,其中的键允许是任何类型。与 Set 相似,通过调用 `Object.is()`方法来判断重复的键,这意味着能将数值 5 与字符串 "5" 作为两个相对独立的键。使用`set()` 方法能将任何类型的值关联到某个键上,并且该值此后能用 `get()` 方法提取出来。Map 也拥有一个 `size` 属性与一个 `forEach()` 方法,让项目访问更容易。

49、 0.1 + 0.2 为什么不等于 0.3, 在项目中遇到要怎么处理 ?

计算机内部存储数据使用2进制存储,两个数字进行的数学运算,首先是将这两个数字以2进制形式,存储在计算机内部,然后在计算机内部使用两个2进制数字进行计算,最后将计算结果的2进制数字转为10进制展示出来。

由于10进制的小数在转2进制的时候,规则是小数部分乘以2,判断是否得到一个整数,如果得到整数,转换完成;如果没有得到整数,则继续乘以2判断。所以,0.1和0.2在转换2进制的时候,其实是一个无限死循环,也就是一直乘以2没有得到整数的时候,但计算机内部对于无限死循环的数据,会根据一个标准保留52位。也就是说,计算机内部在存储0.1和0.2的时候,本来就不精准,两个不精准的小数在计算后,距离精准的结果是有一定误差的。

项目中碰到这种情况,有3种处理方法:

  • 将小数乘以10的倍数,转为整数,然后计算,计算完成后,再缩小10的倍数,例如:
var result = ((0.1 * 10) + (0.2 * 10)) / 10
    // result === 0.3           
  • 使用数字的toFixed方法,强制保留小数点后多少位,例
var result = (0.1 + 0.2).toFixed(2)
   // result === 0.30           
  • 自定义数字运算方法,当需要进行数学运算的时候,不直接进行,调用自定义的方法进行,例:(加法封装)
function add(...args){
         var num = args.find(item => {
             if(item != 0 && !item){
                throw new Error("数学运算要使用数字")
             }
         })
         var arr = args.map(item => {
             var index = (item+'').indexOf('.')
             if(index >= 0){
                 return (item+'').split('.')[1].length
             }
         })
         arr = arr.filter(item => item)
         if(arr.length){
             var max = Math.max(...arr)
             var data = args.map(item => item * Math.pow(10, max))
             var data.reduce((a, b) => a + b) / Math.pow(10, max)
         }else{
             var data = args
             return data.reduce((a, b) => a + b)
         }
     }
     // 调用使用:
     var num1 = add(0.1, 0.2)
     console.log(num1); // 0.3

     var num2 = add(1, 2)
     console.log(num2); // 3

     var num3 = add(1, 2.1)
     console.log(num3); // 3.1           

50、 什么是模块化思想 ?

就是JS中将不同功能的代码封装在不同的文件中, 互相引用时不会发生命名冲突的一种思想, 大多数情况下, 一个文件就是一个模块

模块化的实现,有多种方案:

  • CommonJS:
  • CommonJS是nodejs中使用的模块化规范在 nodejs 应用中每个文件就是一个模块,拥有自己的作用域,文件中的变量、函数都是私有的,与其他文件相隔离。模块导出:module.exports=数据,模块导入:require('模块文件路径')
  • ES6的模块化:
  • 模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
  • 一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export命令输出变量。
  • AMD (Asynchronous Module Definition):
  • 特点: 提倡依赖前置,在定义模块的时候就要声明其依赖的模块:导入模块require([module],callback);定义模块:define('模块名称', 函数)。
  • CMD (Common Module Definition):
  • CMD规范是国内SeaJS的推广过程中产生的。提倡就近依赖(按需加载),在用到某个模块的时候再去require。定义模块:define(function (require, exports, module) {}),使用模块:seajs.use()

51、 说说怎么用js 写无缝轮播图

将所有需要轮播的内容动态复制一份,放在原本的容器中,加定时器让整个容器中的内容滚动轮播,当内容轮播到left值为-原本的内容宽度时,快速将内容切换到left值为0的状态。

52、 JS 如何实现多线程 ?

我们都知道JS是一种单线程语言,即使是一些异步的事件也是在JS的主线程上运行的(具体是怎么运行的,可以看我另一篇博客JS代码运行机制)。像setTimeout、ajax的异步请求,或者是dom元素的一些事件,都是在JS主线程执行的,这些操作并没有在浏览器中开辟新的线程去执行,而是当这些异步操作被操作时或者是被触发时才进入事件队列,然后在JS主线程中开始运行。

首先说一下浏览器的线程,浏览器中主要的线程包括,UI渲染线程,JS主线程,GUI事件触发线程,http请求线程。

JS作为脚本语言,它的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。(这里这些问题我们不做研究)

但是单线程的语言,有一个很致命的确定。如果说一个脚本语言在执行时,其中某一块的功能在执行时耗费了大量的时间,那么就会造成阻塞。这样的项目,用户体验是非常差的,所以这种现象在项目的开发过程中是不允许存在的。

其实JS为我们提供了一个Worker的类,它的作用就是为了解决这种阻塞的现象。当我们使用这个类的时候,它就会向浏览器申请一个新的线程。这个线程就用来单独执行一个js文件。 var worker = new Worker(js文件路径);那么这个语句就会申请一个线程用来执行这个js文件。这样也就实现了js的多线程。

53、 闭包的使用场景 ?

一个函数被当作值返回时,也就相当于返回了一个通道,这个通道可以访问这个函数词法作用域中的变量,即函数所需要的数据结构保存了下来,数据结构中的值在外层函数执行时创建,外层函数执行完毕时理因销毁,但由于内部函数作为值返回出去,这些值得以保存下来。而且无法直接访问,必须通过返回的函数。这也就是私有性。

本来执行过程和词法作用域是封闭的,这种返回的函数就好比是一个虫洞,开了挂。

闭包的形成很简单,在执行过程完毕后,返回函数,或者将函数得以保留下来,即形成闭包。

  • 防抖:
function debounce(fn, interval) {
       let timer = null; // 定时器
       return function() {
           // 清除上一次的定时器
           clearTimeout(timer);
           // 拿到当前的函数作用域
           let _this = this;
           // 拿到当前函数的参数数组
           let args = Array.prototype.slice.call(arguments, 0);
           // 开启倒计时定时器
           timer = setTimeout(function() {
               // 通过apply传递当前函数this,以及参数
               fn.apply(_this, args);
               // 默认300ms执行
           }, interval || 300)
       }
   }           
  • 节流:
function throttle(fn, interval) {
      let timer = null; // 定时器
      let firstTime = true; // 判断是否是第一次执行
      // 利用闭包
      return function() {
          // 拿到函数的参数数组
          let args = Array.prototype.slice.call(arguments, 0);
          // 拿到当前的函数作用域
          let _this = this;
          // 如果是第一次执行的话,需要立即执行该函数
          if(firstTime) {
              // 通过apply,绑定当前函数的作用域以及传递参数
              fn.apply(_this, args);
              // 修改标识为null,释放内存
              firstTime = null;
          }
          // 如果当前有正在等待执行的函数则直接返回
          if(timer) return;
          // 开启一个倒计时定时器
          timer = setTimeout(function() {
              // 通过apply,绑定当前函数的作用域以及传递参数
              fn.apply(_this, args);
              // 清除之前的定时器
              timer = null;
              // 默认300ms执行一次
          }, interval || 300)
      }
  }           
  • 迭代器:
var arr =['aa','bb','cc'];
   function incre(arr){
       var i=0;
       return function(){
           //这个函数每次被执行都返回数组arr中 i下标对应的元素
            return arr[i++] || '数组值已经遍历完';
       }
   }
   var next = incre(arr);
   console.log(next());//aa
   console.log(next());//bb
   console.log(next());//cc
   console.log(next());//数组值已经遍历完           
  • 缓存:
var fn=(function(){
           var cache={};//缓存对象
           var calc=function(arr){//计算函数
               var sum=0;
               //求和
               for(var i=0;i<arr.length;i++){
                   sum+=arr[i];
               }
               return sum;
           }

           return function(){
               var args = Array.prototype.slice.call(arguments,0);//arguments转换成数组
               var key=args.join(",");//将args用逗号连接成字符串
               var result , tSum = cache[key];
               if(tSum){//如果缓存有
                   console.log('从缓存中取:',cache)//打印方便查看
                   result = tSum;
               }else{
                   //重新计算,并存入缓存同时赋值给result
                   result = cache[key]=calc(args);
                   console.log('存入缓存:',cache)//打印方便查看
               }
               return result;
           }
        })();
       fn(1,2,3,4,5);
       fn(1,2,3,4,5);
       fn(1,2,3,4,5,6);
       fn(1,2,3,4,5,8);
       fn(1,2,3,4,5,6);           
  • getter和setter:
function fn(){
          var name='hello'
          setName=function(n){
              name = n;
          }
          getName=function(){
              return name;
          }

          //将setName,getName作为对象的属性返回
          return {
              setName:setName,
              getName:getName
          }
      }
      var fn1 = fn();//返回对象,属性setName和getName是两个函数
      console.log(fn1.getName());//getter
          fn1.setName('world');//setter修改闭包里面的name
      console.log(fn1.getName());//getter           
  • 柯里化:
function curryingCheck(reg) {
      return function(txt) {
          return reg.test(txt)
      }
  }

  var hasNumber = curryingCheck(/\d+/g)
  var hasLetter = curryingCheck(/[a-z]+/g)

  hasNumber('test1')      // true
  hasNumber('testtest')   // false
  hasLetter('21212')      // false           
  • 循环中绑定事件或执行异步代码
var p1 = "ss";
   var p2 = "jj";
   function testSetTime(para1,para2){
       return (function(){
           console.log(para1 + "-" + para2);
       })
   }
   var test = testSetTime(p1, p2);
   setTimeout(test, 1000);
   setTimeout(function(){
       console.log(p1 + "-" + p2)
   },1000)           
  • 单例模式:
var Singleton = (function () {
      var instance;

      function createInstance() {
          return new Object("I am the instance");
      }

      return {
          getInstance: function () {
              if (!instance) {
                  instance = createInstance();
              }
              return instance;
          }
      };
  })();           

54、 常见的兼容问题有哪些 ?

  • 获取标签节点:
  • document.getElementsByClassName('类名')在低版本ie中不兼容。解决方法是使用其他方式获取:
document.getElementById('id名')
   document.getElementsByTagName('标签名')
   document.getElementsByName('name属性值')
   document.querySelector('css选择器')
   document.querySelectorAll('css选择器')           

获取卷去的高度

// 当有文档声明的时候
document.documentElement.scrollTop
document.documentElement.srollLeft
// 没有文档声明的时候
document.body.scrollTop
document.body.scrollLeft           

解决办法使用兼容写法:

// 获取
var t = document.documentElement.scrollTop || document.body.scrollTop
var l = document.documentElement.srollLeft || document.body.scrollLeft
// 设置
document.documentElement.scrollTop = document.body.scrollTop = 数值
document.documentElement.srollLeft = document.body.scrollLeft = 数值           

获取样式

// W3C标准浏览器
    window.getComputedStyle(元素)
    // 低版本IE中
    元素.currentStyle           

使用函数封装的方式兼容:

function getStyle(ele,attr){
        if(window.getComputedStyle){
           return getComputedStyle(ele)[attr]
        }else{
            return ele.currentStyle[attr]
        }
    }           

事件侦听器

// W3C浏览器
  ele.addEventListener(事件类型,函数)
  // 低版本Ie
  ele.attachEvent('on事件类型',函数)           

使用函数封装的方式解决:

function bindEvent(ele,type,handler){
      if(ele.addEventListener){
          ele.addEventListener(type,handler)
      }else if(ele.attachEvent){
          ele.attachEvent('on'+type,handler)
      }else{
          ele['on'+type] = handler
      }
  }           

事件解绑

// W3C浏览器
  ele.removeEventListener(事件类型,函数)
  // 低版本Ie
  ele.detachEvent('on事件类型',函数)           

使用函数封装的方式解决:

function unBind(ele,type,handler){
        if(ele.removeEventListener){
            ele.removeEventListener(type,handler)
        }else if(ele.detachEvent){
            ele.detachEvent('on'+type,handler)
        }else{
            ele['on'+type] = null
        }
    }           

事件对象的获取

// W3C浏览器
   元素.on事件类型 = function(e){}
   元素.addEventListener(事件类型,fn)
   function fn(e){

   }
   // 在低版本IE中
   元素.on事件类型 = function(){ window.event }
   元素.addEventListener(事件类型,fn)
   function fn(){
       window.event
   }           

使用短路运算符解决:

元素.on事件类型 = function(e){
        var e = e || window.event
    }
    元素.addEventListener(事件类型,fn)
    function fn(e){
        var e = e || window.event
    }           

阻止默认行为

// W3C浏览器
   元素.on事件类型 = function(e){
       e.preventDefault()
   }
   // 在低版本IE中
   元素.on事件类型 = function(){ window.event.returnValue = false }           

通过封装函数解决

元素.on事件类型 = function(e){
         var e = e || window.event
         e.preventDefault?e.preventDefault():e.returnValue=false
     }           

阻止事件冒泡

// W3C浏览器
     元素.on事件类型 = function(e){
         e.stopPropagation()
     }
     // 在低版本IE中
     元素.on事件类型 = function(){ window.event.cancelBubble = true }           

通过函数封装解决:

元素.on事件类型 = function(e){
     var e = e || window.event
     e.stopPropagation?e.stopPropagation():e.cancelBubble=true
 }           

获取精准的目标元素

// W3C浏览器
    元素.on事件类型 = function(e){
        e.target
    }
    // 在低版本IE中
    元素.on事件类型 = function(){ window.event.srcElement }           

通过短路运算符解决:

元素.on事件类型 = function(e){
        var e = e || window.event
        var target = e.target || e.srcElement;
    }           

获取键盘码

// W3C浏览器
     元素.on事件类型 = function(e){
         e.keyCode
     }
     // 在低版本火狐中
     元素.on事件类型 = function(e){
       e.which
     }           

通过短路运算符解决:

元素.on事件类型 = function(e){
    var e = e || window.event
    var keycode = e.keyCode || e.which;
}           

55、 在 JS 中如何阻止事件冒泡 ?

使用事件对象阻止事件冒泡,以前的w3c浏览器中,使用事件对象的方法阻止:

事件对象.stopPropagation()

在ie低版本浏览器中,使用事件对象的属性阻止:

事件对象.cancelBubble = true

现在的w3c浏览器也支持ie低版本浏览器中的写法,所以以前在阻止事件冒泡的时候,需要考虑兼容写法,现在就不需要了,直接用ie低版本浏览器中的写法即可。

56、两个数组 var A = [1, 5, 6]; var B = [2, 6, 7],实现一个方法,找出仅存在于A 或者 仅 存在于B中的所有数字。

function getDiff(arr, brr){
    // 仅存在于arr中的内容
      var onlyArr = arr.filter(item => !brr.some(v => item === v))
      // 仅存在于brr中的内容
      var onlyBrr = brr.filter(v => !arr.some(item => v === item))
      // 需要哪个就返回哪个,或者一起返回
      return {
          "仅存在于arr中的内容": onlyArr,
          "仅存在于brr中的内容": onlyBrr
      }
  }           

57、 你了解构造函数吗 ? class 是什么 ? 两者有什么区别 ?

在es5中构造函数其实就是在定义一个类,可以实例化对象,es6中class其实是构造函数的语法糖。但还是有点区别的:

  • 在class内部和class的方法内部,默认使用严格模式
  • class类不存在预解析,也就是不能先调用class生成实例,再定义class类,但是构造函数可以。
  • class中定义的方法默认不能被枚举,也就是不能被遍历。
  • class必须使用new执行,但是构造函数没有new也可以执行。
  • class中的所有方法都没有原型,也就不能被new
  • class中继承可以继承静态方法,但是构造函数的继承不能。

58、是否存在a的值(a==0 && a==1)为true 的情况 ?

var value = -1
   Object.defineProperty(window,'a',{
       get(){
           return value+=1;
       }
   })
   if(a===0&&a===1){ // true
       console.log('success')
   }           

59、for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 1000); } 要求:输出0,1,2,3,4

首先这个面试题考察的是对于js中异步代码以及作用域的理解:

js中常见的异步代码包括定时器和ajax。js执行代码的流程是碰到同步代码就执行,碰到异步就交给浏览器的webAPI处理,当webAPI中的异步该执行时,webAPI会将需要执行的回调函数放在任务队列中,等候执行,所以,js中所有的异步代码总会在所有同步代码执行结束后,再执行任务队列中的代码。

在这个问题中,循环是同步代码,定时器是异步代码,所以整个循环都执行结束以后才会执行定时器代码。

for循环中使用var定义的变量是全局变量,定时器回调函数中输出变量的时候,根据作用域规则,先在当前作用域中变量i的定义表达式,如果没有找到,就去上一级作用域中找,此时,在局部作用域中没有找到,去上级作用域中,也就是全局找到了,全局中的i,因为循环已经执行结束了,所以i的值是5。

最终,会输出5个5。

其次考察的是对于类似问题的解决方式,间接性判断你是否有过类似情况的开发:

这个问题的解决思路就是让回调函数中输出i的时候,不要去全局中找i,因为全局的i在循环执行结束后已经变成5了,根据这个思路,有2种解决办法:

  • 在异步代码外面嵌套一层函数作用域
for(var i = 0;i < 5; i++){
    (function(i) {
        setTimeout(function() {
            console.log(i)
        }, 1000)
    })(i)
}           

原理是自调用函数会产生作用域,循环5次就会产生5个作用域,每个作用域代码在执行的时候都有形参i传递。所以每个作用域中的i都是不同的,分别是:0 1 2 3 4。当作用域中的异步代码执行的时候,自己作用域中没有i变量的定义,然后上级作用域就是自调用函数的作用域,找到了单独的i。最终可以输出:0 1 2 3 4

  1. 将循环代码中的var换成es6的let
for(let i = 0;i < 5; i++){
         setTimeout(function() {
             console.log(i)
         }, 1000)
     }           

es6的let自带块级作用域,原理跟第一种解决思路是一样的,转成es5后,代码是一样的。

60、实现一个 add 方法 使计算结果能够满足如下预期: - add(1)(2)(3)() = 6 - add(1,2,3)(4)() = 10

function add (...args) {
    if(args.length === 3){
        return -(args[0] * args[1] * 2 + args[2] * 2)
    }else{
        return -args[args.length-1]
    }

}

function currying (fn) {
  let args = []
  return function _c (...newArgs) {
    if (newArgs.length) {
      args = [
        ...args,
        ...newArgs
      ]
      return _c
    } else {
      return fn.apply(this, args)
    }
  }
}
let addCurry = currying(add)

var a = addCurry(1)(2)(3)()
console.log(-a); // 10

var b = addCurry(1,2,3)(4)()
console.log(6 - b); // 10           

继续阅读