JS面试真题 part6
- 26、如何判断一个元素是否在可视区域中
 - 27、什么是单点登录?如何实现
 - 28、 如何实现上拉加载,下拉刷新
 - 29、说说你对正则表达式的理解?应用场景?
 - 30、说说你对函数式编程的理解?优缺点
 
26、如何判断一个元素是否在可视区域中
自己回答:根据当前位置距离浏览器顶部的y轴距离x轴距离和浏览器显示屏的高度宽度进行对比
 标准回答:
 常用三种方式:
- offsetTop、scrollTop、可视区高度
 - getBoundingClientRect
 - Intersection Observer
 
1、offsetTop、scrollTop、可视区高度
主要关系:el.offsetTop - document.documentElement.scrollTop <= viewPortHeight
 offsetTop:元素的上边框至包含元素的上内边框之间的像素距离
 scrollTop:滚动条滚动距离
将元素的scrollTop和scrollLeft设置为0,可以重置元素的滚动位置
viewPortHeight:可视区高度、设备高度。
代码实现:
 function isInViewPortOfOne (el) {
 // viewPortHeight 
 const viewPortHeight = window.innerHeight || document.documentElement.c
lientHeight || document.body.clientHeight
 const offsetTop = el.offsetTop
 const scrollTop = document.documentElement.scrollTop
 const top = offsetTop - scrollTop
 return top <= viewPortHeight
}
 
2、getBoundingClientRect
返回值是一个DOMReact对象,拥有left,top,right,bottom,x,y,width和height属性
 const target = document.querySelector('.target');
const clientRect = target.getBoundingClientRect();
console.log(clientRect);
// {
// bottom: 556.21875,
// height: 393.59375,
// left: 333,
// right: 1017,
// top: 162.625,
// width: 684
// }
 

 如果一个元素在视窗之内的话,那么它一定满足下面四个条件
- top大于等于0
 - left大于等于0
 - bottom小于等于视窗高度
 - right小于等于视窗宽度
 
实现代码:
function isInViewPort(element) {
 const viewWidth = window.innerWidth || document.documentElement.clientWi
dth;
 const viewHeight = window.innerHeight || document.documentElement.client
Height;
 const {
 top,
 right,
 bottom,
 left,
 } = element.getBoundingClientRect();
 return (
 top >= 0 &&
 left >= 0 &&
 right <= viewWidth &&
 bottom <= viewHeight
 );
}
 
3、 Intersection Observer
Intersection Observer 重叠观察者,用于判断两个元素是否重叠,因为不用进行事件的监听,性能方面比 getBoundingClientRect会好很多
使用步骤主要分为两步:创建观察者和传入被观察者
创建观察者
 const options = {
 // 表示重叠面积占被观察者的比例,从0 - 1取值 
 // 1 表示完全被包含
 threshold: 1.0,
 root:document.querySelector('#scrollArea') // 必须是目标元素的父级元素
};
const callback = (entries, observer) => { ....}
const observer = new IntersectionObserver(callback, options);
 
通过new IntersectionObserver创建了观察者observer,传入的参数 callback在重叠比例超过threshold时会被执行
关于callback回调函数常用属性如下:
// callback
const callback = function(entries, observer) {
 entries.forEach(entry => {
 entry.time; // 触发的时间
 entry.rootBounds; // 根元素的位置矩形,这种情况下为视窗位置
 entry.boundingClientRect; // 被观察者的位置矩形
 entry.intersectionRect; // 重叠区域的位置矩形
 entry.intersectionRatio; // 重叠区域占被观察者面积的比例(被观察者不是矩形时也按矩形计算)
 entry.target; // 被观察者
 });
};
 
传入被观察者
 通过 observer.observer(target) 这一行代码即可注册被观察者
const target = document.querySelector('.target');
observer.observe(target);
 
27、什么是单点登录?如何实现
标准回答:
什么是单点登录?
单点登录(Single Sign On),简称SSO,是目前比较流行的企业业务整合的解决方案之一
SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有互相信任的应用系统
SSO一般需要一个独立的认证中心(passport),子系统的登录均得通过passport,子系统本身将不参与登录操作
当一个系统成功登录以后,passport 将会颁发一个令牌给各个子系统,子系统可以拿着令牌获取各自的受保护资源,为了减少频繁认证,各个子系统在被 passport 授权以后,会建立一个局部会话,在一定时间内可以无需再次向 passport 发起认证。
 
 上图有四个系统,当application1、application2、application3需要登录时,将跳到sso系统,sso系统完成登录。其他应用系统也就随之登录了。
如何实现
1、同域名下的单点登录:
cookie的domain属性设置为当前域的父域,并且父域的cookie会被子域所共享。path属性默认为web应用的上下文路径
利用 cookie 的这个特点,只需要将domain属性设置为当前域的父域,同时将cookie的path属性设置为根路径,将Session ID(或Token)保存到父域中。这样所有子域都可以访问这个cookie,不过这要求应用系统的域名建立在一个共同的主域下。
2、不同域名下的单点登录(一):
如果是不同域的情况下Cookie 是不共享的,这里我们可以部署一个认证中心,用于专门处理登录请求的独立的 Web 服务
用户统一在认证中心进行登录,登录成功后,认证中心记录用户的登录状态,并将token写入Cookie (注意这个 Cookie 是认证中心的,应用系统是访问不到的)
应用系统检查当前请求有没有Token,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心
由于这个操作会将认证中心的Cookie 自动带过去,因此,认证中心能够根据:Cookie 知道用户是否已经登录过了
如果认证中心发现用户尚未登录,则返回登录页面,等待用户登录
如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标URL,并在跳转前生成一个Token,拼接在目标 URL的后面,回传给目标应用系统
应用系统拿到 Token 之后,还需要向认证中心确认下Token的合法性,防止用户伪造。确认无误后,应用系统记录用户的登录状态,并将Token写入Cookie然后给本次访问放行。(注意这个Cookie 是当前应用系统的)
当用户再次访问当前应用系统时,就会自动带上这个Token,应用系统验证 Token 发现用户已登录,:于是就不会有认证中心什么事了
此种实现方式相对复杂,支持跨域,扩展性好,是单点登录的标准做法
3、不同域名下的单点登录(二):
可以选择将 Session ID(或 Token )保存到浏览器的 LocalStorage 中,让前端在每次向后端发送请求时,主动将 LocalStorage 的数据传递给服务端
这些都是由前端来控制的,后端需要做的仅仅是在用户登录成功后,将SessionID(或 Token)放在响应体中传递给前端
单点登录完全可以在前端实现。前端拿到SessionID(或 Token )后,除了将它写入自己的LocalStorage 中之外,还可以通过特殊手段将它写入多个其他域下的LocalStorage中
 关键代码如下:
//获取 token
var token = result.data.token;
// 动态创建一个不可见的iframe,在 iframe中加载一个跨域的 HTML
var iframe = document.createElement("iframe");
iframe.src = "http://app1.com/localstorage.html";
document.body.append(iframe);
// 使用postMessage()方法将token传递给iframe
setTimeout(function () {
 iframe.contentWindow.postMessage(token, "http://app1.com");
}, 4000);
setTimeout(function () {
 iframe.remove();
}, 6000);
//在这个iframe所加载的HTML中绑定一个事件监听器,当事件被触发时,把接收到的token数据写入localStorage
window.addEventListener('message', function (event) {
 localStorage.setItem('token', event.data)
}, false);
 
前端通过 iframe +postMessage()方式,将同一份 Token 写入到了多个域下的 Localstorage 中,前端每次在向后端发送请求之前,都会主动从 LocalStorage 中读取 Token 并在请求中携带,这样就实现了同一份 Token 被多个域所共享
 此种实现方式完全由前端控制,几乎不需要后端参与,同样支持跨域
28、 如何实现上拉加载,下拉刷新
自己回答:一般存在于移动端,uniapp里有对应的api调用。原生的话,需要根据手指移动的距离和屏幕的高度进行对比,判断是向下滑还是向上拉
标准回答:
 下拉刷新和上拉加载这两种交互方式通常出现在移动端中,本质上等同于pc网页中的分页,只是交互形式不同。
 开源社区也有很多优秀的解决方案,如 iscroll、better-scroll、pulltorefresh.js库
上拉加载实现原理:
 
 上拉加载的本质是页面触底,或者快要触底的动作
 首先了解几个属性
scrollTop:滚动视窗的高度距离window顶部的距离,它会随着往上滚动而不断增加,初始值是0,它是一个变化的值clientHeight:它是一个定值,表示屏幕可视区域的高度scrollHeight:页面不能滚动时也是存在的,此时scrollHeight等于clientHeight。scrollHeight表示body所有元素的总长度(包括body元素自身的padding)
综上得出触底公式:
 scrollTop + clientHeight >= scrollHeight
简单实现
 let clientHeight = document.documentElement.clientHeight; //浏览器高度
let scrollHeight = document.body.scrollHeight;
let scrollTop = document.documentElement.scrollTop;
let distance = 50; // 距离视窗还有50的时候,开始触发
if ((scrollTop + clientHeight) >= (scrollHeight - distance)) {
 console.log("开始加载数据");
}
 
下拉刷新实现原理:
下拉刷新的本质是页面本身置于顶部时,用户下拉时需要触发的动作
 关于下拉刷新的原生实现,主要分为三步:
- 监听原生 
touchstart事件,记录其初始位置的值,e.touches[0].pageY; - 监听原生 
touchmove事件,记录并计算当前滑动的位置值与初始位置值的差值,大于0表示向下拉动,并借助CSS3的translateY属性使元素跟随手势向下滑动对应的差值,同时也应设置一个允许滑动的最大值; - 监听原生 
touchend事件,若此时元素滑动达到最大值,则触发callback,同时将translateY重设为0,元素回到初始位置。 
代码实现,html结构
 <main>
 <p class="refreshText"></p >
 <ul id="refreshContainer">
 <li>111</li>
 <li>222</li>
 <li>333</li>
 <li>444</li>
 <li>555</li>
 ...
 </ul>
</main>
 
监听touchstart事件,记录初始的值
var _element = document.getElementById('refreshContainer'),
 _refreshText = document.querySelector('.refreshText'),
 _startPos = 0, // 初始的值
 _transitionHeight = 0; // 移动的距离
_element.addEventListener('touchstart', function(e) {
 _startPos = e.touches[0].pageY; // 记录初始位置
 _element.style.position = 'relative';
 _element.style.transition = 'transform 0s';
}, false);
 
监听touchmove事件,记录滑动差值
_element.addEventListener('touchmove', function(e) {
 // e.touches[0].pageY 当前位置
 _transitionHeight = e.touches[0].pageY - _startPos; // 记录差值
 if (_transitionHeight > 0 && _transitionHeight < 60) {
 _refreshText.innerText = '下拉刷新';
 _element.style.transform = 'translateY('+_transitionHeight+'px)';
 if (_transitionHeight > 55) {
 _refreshText.innerText = '释放更新';
 }
 } 
}, false);
 
最后,就是监听touchend离开的事件
_element.addEventListener('touchend', function(e) {
 _element.style.transition = 'transform 0.5s ease 1s';
 _element.style.transform = 'translateY(0px)';
 _refreshText.innerText = '更新中...';
 // todo...
}, false);
 
从上面可以看到,在下拉到松手的过程中,经历了三个阶段
- 当前手势滑动位置与初始位置差值大于零时,提示正在进行下拉刷新操作
 - 下拉到一定值时,显示松手释放后的操作提示
 - 下拉到达设定最大值松手时,执行回调,提示正在进行更新操作
 
29、说说你对正则表达式的理解?应用场景?
自己回答:需要匹配某种对应的规则的时候.。应用场景:比如手机号码验证,中文名字验证等
标准回答:
正则表达式是一种用来匹配字符串的强有力的武器,它的设计思想是用一种描述性的语言定义一个规则,凡是符合规则的字符串,就认为它“匹配”,否则该字符串就是不合法的。
 构建方法有两种
 1、字面量创建,其由包含在斜杠之间的模式组成
const re = /\d+/g;
 
2、调用RegExp对象的构造函数
const re = new RegExp("\\d+","g");
const rul = "\\d+"
const re1 = new RegExp(rul,"g");
 
使用构造函数创建,第一个参数可以是一个变量,遇到特殊字符 \ 需要使用 \\ 进行转义
应用场景:
验证QQ合法性(5~15位、全是数字、不以0开头)
const reg = /^[1-9][0-9]{4,14}$/
const isvalid = patrn.exec(s)
 
校验用户账号合法性(只能输入5-20个以字母开头、可带数字、“_”、“.”的字符串)
var patrn=/^[a-zA-Z]{1}([a-zA-Z0-9]|[._]){4,19}$/;
const isvalid = patrn.exec(s)
 
将url参数解析为对象
30、说说你对函数式编程的理解?优缺点
自己回答:面向过程的编程,去对象化,更关注函数功能,使函数更简洁,不依赖对象。优点:功能集中化,缺点:失去对象的关联性
 标准回答:
函数式编程是一种“编程范式"(programming paradigm),一种编写程序的方法论
主要的编程范式有三种:命令式编程,声明式编程和函数式编程
相比命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而非设计一个复杂的执行过程
举个例子,将数组每个元素进行平方操作,命令式编程与函数式编程如下
// 命令式编程
var array = [0, 1, 2, 3]
for(let i = 0; i < array.length; i++) {
 array[i] = Math.pow(array[i], 2)
}
// 函数式编程
[0, 1, 2, 3].map(num => Math.pow(num, 2))
 
简单来讲,就是要把过程逻辑写成函数,定义好输入参数,只关心它的输出结果
 即是一种描述集合和集合之间的转换关系,输入通过函数都会返回有且只有一个输出值

 可以看到,函数实际上是一个关系,或者说是一种映射,而这种映射关系是可以组合的,一旦我们知道一个函数的输出类型可以匹配另一个函数的输入,那他们就可以进行组合
了解几个相关概念
1、纯函数:
函数式编程旨在尽可能的提高代码的无状态性和不变性。要做到这一点,就要学会使用无副作用的函数,也就是纯函数
 纯函数是对给定的输入返回相同输出的函数,并且要求你所有的数据都是不可变的,即纯函数=无状态+数据不可变
 
 举一个简单的例子
let double = value=>value*2;
 
特性:
- 函数内部传入指定的值,就会返回确定唯一的值
 - 不会造成超出作用域的变化,例如修改全局变量或引用传递的参数
 
优势:
- 使用纯函数,我们可以产生可测试的代码
 - 不依赖外部环境计算,不会产生副作用,提高函数的复用性
 - 可读性更强,函数不管是否是纯函数,都会有一个语义化的名称,更便于阅读
 - 可以组装成复杂任务的可能性。符合模块化概念及单一职责原则
 
2、高阶函数:
 在我们的编程世界中,我们需要处理的其实也只有“数据”和“关系”,而关系就是函数。
 编程工作也就是在找一种映射关系,一旦关系找到了,问题就解决了。
 
 在这里,就是高阶函数的作用,高级函数,就是以函数作为输入或输出的函数被称为高阶函数
 通过高阶函数抽象过程,注重结果,如下面例子
 const forEach = function(arr,fn){
 for(let i=0;i<arr.length;i++){
 fn(arr[i]);
 }
}
let arr = [1,2,3];
forEach(arr,(item)=>{
 console.log(item);
})
 
上面通过高阶函数 forEach 来抽象循环如何做的逻辑,直接关注做了什么
 高阶函数存在缓存的特性,主要是利用闭包作用
 const once = (fn)=>{
	 let done = false;
	 return function(){
		 if(!done){
			 fn.apply(this,fn);
		 }else{
			 console.log("该函数已经执行");
		 }
		 done = true;
	 }
 }
 
3、柯里化:
柯里化是把一个多参数函数转化成一个嵌套的一元函数的过程
一个二元函数如下:
let fn = (x,y)=>x+y;
 
转化成柯里化函数如下:
const curry = function(fn){
	 return function(x){
		 return function(y){
			 return fn(x,y);
		 }
	 }
}
let myfn = curry(fn);
console.log( myfn(1)(2) );
 
上面的curry函数只能处理二元情况,下面来实现一个实现多参数的情况
// 多参数柯里化
const curry = function(fn){
	 return function curriedFn(...args){
		 if(args.length<fn.length){
			 return function(){
				 return curriedFn(...args.concat([...arguments]));
			 }
		 }
		 return fn(...args);
	 }
}
const fn = (x,y,z,a)=>x+y+z+a;
const myfn = curry(fn);
console.log(myfn(1)(2)(3)(1));
 
关于柯里化函数的意义如下:
- 让纯函数更纯,每次接受一个参数,松散解耦
 - 惰性执行
 
4、组合与管道:
组合函数,目的是将多个函数组合成一个函数
 举个简单的例子:
 function afn(a){
    return a*2;
}
function bfn(b){
    return b*3;
}
const compose = (a,b)=>c=>a(b(c));
let myfn = compose(afn,bfn);
console.log( myfn(2));
 
compose 实现一个简单的功能:形成了一个新的函数,而这个函数就是一条从bfn->afn 的流水线
如何实现一个多函数组合:
const compose = (...fns)=>val=>fns.reverse().reduce((acc,fn)=>fn(acc),val);
 
compose从右边到左,而管道函数pipe从左到右执行
const pipe = (...fns)=>val=>fns.reduce((acc,fn)=>fn(acc),val);
 
组合函数与管道函数的意义在于:可以把很多小函数组合起来完成更复杂的逻辑
优缺点
优点:
- 更好的管理状态:因为它的宗旨是无状态,或者说更少的状态,能最大化的减少这些未知、优化代码、减少出错情况
 - 更简单的复用:固定输入->固定输出,没有其他外部变量影响,并且无副作用。这样代码复用时,完全不需要考虑它的内部实现和外部影响
 - 更优雅的组合:往大的说,网页是各个组件组成的,往小的说,一个函数也可能是由多个小函数组成的。更强的复用性,带来更强大的组合性
 - 隐藏好处。减少代码量,提高维护性
 
缺点:
- 性能:函数式编程相对于指令式编程,性能绝对是一个短板,因为它往往会对一个方法进行过度包装,从而产生上下文切换的性能开销
 - 资源占用:在JS中为了实现对象状态的不可变,往往会创建新的对象,因此,它对垃圾回收所产生的压力远远超过其他编程方式
 - 递归陷阱:在函数式编程中,为了实现迭代,通常会采用递归操作
 



















