JS面试真题 part6
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中为了实现对象状态的不可变,往往会创建新的对象,因此,它对垃圾回收所产生的压力远远超过其他编程方式
- 递归陷阱:在函数式编程中,为了实现迭代,通常会采用递归操作