当前位置: 首页 > article >正文

JS面试真题 part6

JS面试真题 part6

  • 26、如何判断一个元素是否在可视区域中
  • 27、什么是单点登录?如何实现
  • 28、 如何实现上拉加载,下拉刷新
  • 29、说说你对正则表达式的理解?应用场景?
  • 30、说说你对函数式编程的理解?优缺点

26、如何判断一个元素是否在可视区域中

自己回答:根据当前位置距离浏览器顶部的y轴距离x轴距离和浏览器显示屏的高度宽度进行对比
标准回答:
常用三种方式:

  1. offsetTop、scrollTop、可视区高度
  2. getBoundingClientRect
  3. 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、同域名下的单点登录:

cookiedomain属性设置为当前域的父域,并且父域的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网页中的分页,只是交互形式不同。
开源社区也有很多优秀的解决方案,如 iscrollbetter-scrollpulltorefresh.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表示向下拉动,并借助CSS3translateY属性使元素跟随手势向下滑动对应的差值,同时也应设置一个允许滑动的最大值;
  • 监听原生 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中为了实现对象状态的不可变,往往会创建新的对象,因此,它对垃圾回收所产生的压力远远超过其他编程方式
  • 递归陷阱:在函数式编程中,为了实现迭代,通常会采用递归操作

http://www.kler.cn/a/319631.html

相关文章:

  • flink sql + kafka + mysql 如何构建实时数仓
  • 24/11/13 算法笔记<强化学习> DQN算法
  • 机器学习day3-KNN算法、模型调优与选择
  • Java设计模式面试题及参考答案
  • 开源 2 + 1 链动模式、AI 智能名片、S2B2C 商城小程序在用户留存与品牌发展中的应用研究
  • 定时器(QTimer)与随机数生成器(QRandomGenerator)的应用实践——Qt(C++)
  • 【C++】模拟实现list
  • WPF DataGrid 动态修改某一个单元格的样式
  • MyBatis 缓存机制
  • 【AI写作】解释区块链技术的应用场景和优势
  • select 函数简介
  • CentOS Linux教程(6)--CentOS目录
  • CSS的字体属性
  • 软件测试面试题(6)——二面(游戏测试)
  • 2024年_ChatGPT 及类似的人工智能技术带来的影响与改变 怎样利用 ChatGPT 提高学习效率
  • 在SpringBoot项目中利用Redission实现布隆过滤器(布隆过滤器的应用场景、布隆过滤器误判的情况、与位图相关的操作)
  • MICS:PythonJail沙箱逃逸(持续更新中)
  • Python数据分析与可视化:从基础到高级应用
  • vue3 实现图片预览组件
  • [ABC330E] Mex and Update
  • java-重启异常断掉的线程和监控线程状态
  • Android——Application
  • 网红挣钱太容易了
  • 路由器全局配置DHCP实验简述
  • MySQL篇(视图)(持续更新迭代)
  • CANopen通讯协议笔记