Tiny'Wo | 小窝

网络中的一小块自留地

面试官:说说 JavaScript 中内存泄漏的几种情况�?

一、是什�?

内存泄漏(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内�?
并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费

程序的运行需要内存。只要程序提出要求,操作系统或者运行时就必须供给内�?
对于持续运行的服务进程,必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩�?

C语言中,因为是手动管理内存,内存泄露是经常出现的事情�?

1
2
3
4
5
6
char * buffer;
buffer = (char*) malloc(42);

// Do something with buffer

free(buffer);

上面�?C 语言代码,malloc方法用来申请内存,使用完毕之后,必须自己用free方法释放内存�?
这很麻烦,所以大多数语言提供自动内存管理,减轻程序员的负担,这被称为”垃圾回收机制”

二、垃圾回收机�?

Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存

原理:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内�?
通常情况下有两种实现方式�?

  • 标记清除
  • 引用计数

标记清除

JavaScript最常用的垃圾收回机�?
当变量进入执行环境是,就标记这个变量为“进入环境“。进入环境的变量所占用的内存就不能释放,当变量离开环境时,则将其标记为“离开环境�?
垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去�?
在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了

随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内�?
举个例子�?

1
2
3
4
5
var m = 0,n = 19 // �?m,n,add() 标记为进入环境�?add(m, n) // �?a, b, c标记为进入环境�?console.log(n) // a,b,c标记为离开环境,等待垃圾回收�?function add(a, b) {
a++
var c = a + b
return c
}

引用计数

语言引擎有一�?引用�?,保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放

如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄�?

1
2
const arr = [1, 2, 3, 4];
console.log('hello world');

上面代码中,数组[1, 2, 3, 4]是一个值,会占用内存。变量arr是仅有的对这个值的引用,因此引用次数为1。尽管后面的代码没有用到arr,它还是会持续占用内�?
如果需要这块内存被垃圾回收机制释放,只需要设置如下:

1
arr = null

通过设置arrnull,就解除了对数组[1,2,3,4]的引用,引用次数变为 0,就被垃圾回收了

小结

有了垃圾回收机制,不代表不用关注内存泄露。那些很占空间的值,一旦不再用到,需要检查是否还存在对它们的引用。如果是的话,就必须手动解除引用

三、常见内存泄露情�?

意外的全局变量

1
2
3
function foo(arg) {
bar = "this is a hidden global variable";
}

另一种意外的全局变量可能�?this 创建�?

1
2
3
4
function foo() {
this.variable = "potential accidental global";
}
// foo 调用自己,this 指向了全局对象(window�?foo();

上述使用严格模式,可以避免意外的全局变量

定时器也常会造成内存泄露

1
2
3
4
5
6
7
8
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// 处理 node �?someResource
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);

如果id为Node的元素从DOM中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放

包括我们之前所说的闭包,维持函数内局部变量,使其得不到释�?

1
2
3
4
5
6
7
function bindEvent() {
var obj = document.createElement('XXX');
var unused = function () {
console.log(obj, '闭包内引用obj obj不会被释�?);
};
obj = null; // 解决方法
}

没有清理对DOM元素的引用同样造成内存泄露

1
2
3
const refA = document.getElementById('refA');
document.body.removeChild(refA); // dom删除�?console.log(refA, 'refA'); // 但是还存在引用能console出整个div 没有被回�?refA = null;
console.log(refA, 'refA'); // 解除引用

包括使用事件监听addEventListener监听的时候,在不监听的情况下使用removeEventListener取消对事件监�?

参考文�?

面试官:说说new操作符具体干了什么?

一、是什�?

JavaScript中,new操作符用于创建一个给定构造函数的实例对象

例子

1
2
3
4
5
6
7
8
9
10
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.sayName = function () {
console.log(this.name)
}
const person1 = new Person('Tom', 20)
console.log(person1) // Person {name: "Tom", age: 20}
t.sayName() // 'Tom'

从上面可以看到:

  • new 通过构造函�?Person 创建出来的实例可以访问到构造函数中的属�?- new 通过构造函�?Person 创建出来的实例可以访问到构造函数原型链中的属性(即实例与构造函数通过原型链连接了起来�?
    现在在构建函数中显式加上返回值,并且这个返回值是一个原始类�?
    1
    2
    3
    4
    5
    6
    function Test(name) {
    this.name = name
    return 1
    }
    const t = new Test('xxx')
    console.log(t.name) // 'xxx'

可以发现,构造函数中返回一个原始值,然而这个返回值并没有作用

下面在构造函数中返回一个对�?

1
2
3
4
5
6
7
8
function Test(name) {
this.name = name
console.log(this) // Test { name: 'xxx' }
return { age: 26 }
}
const t = new Test('xxx')
console.log(t) // { age: 26 }
console.log(t.name) // 'undefined'

从上面可以发现,构造函数如果返回值为一个对象,那么这个返回值会被正常使�?

二、流�?

从上面介绍中,我们可以看到new关键字主要做了以下的工作�?

  • 创建一个新的对象obj
  • 将对象与构建函数通过原型链连接起�?- 将构建函数中的this绑定到新建的对象obj�?
  • 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处�?
    举个例子�?
    1
    2
    3
    4
    5
    6
    7
    function Person(name, age){
    this.name = name;
    this.age = age;
    }
    const person1 = new Person('Tom', 20)
    console.log(person1) // Person {name: "Tom", age: 20}
    t.sayName() // 'Tom'

流程图如下:

三、手写new操作�?

现在我们已经清楚地掌握了new的执行过�?
那么我们就动手来实现一下new

1
2
3
4
5
6
7
function mynew(Func, ...args) {
// 1.创建一个新对象
const obj = {}
// 2.新对象原型指向构造函数原型对�? obj.__proto__ = Func.prototype
// 3.将构建函数的this指向新对�? let result = Func.apply(obj, args)
// 4.根据返回值判�? return result instanceof Object ? result : obj
}

测试一�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function mynew(func, ...args) {
const obj = {}
obj.__proto__ = func.prototype
let result = func.apply(obj, args)
return result instanceof Object ? result : obj
}
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function () {
console.log(this.name)
}

let p = mynew(Person, "huihui", 123)
console.log(p) // Person {name: "huihui", age: 123}
p.say() // huihui

可以发现,代码虽然很短,但是能够模拟实现new

面试官:JavaScript原型,原型链 ? 有什么特点?

一、原�?

JavaScript 常被描述为一种基于原型的语言——每个对象拥有一个原型对�?
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾

准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非实例对象本身

下面举个例子�?
函数可以有属性�?每个函数都有一个特殊的属性叫作原型prototype

1
2
function doSomething(){}
console.log( doSomething.prototype );

控制台输�?

1
2
3
4
5
6
7
8
9
10
11
12
{
constructor: ƒ doSomething(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}

上面这个对象,就是大家常说的原型对象

可以看到,原型对象有一个自有属性constructor,这个属性指向该函数,如下图关系展示

二、原型链

原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法

在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法

下面举个例子�?

1
2
3
4
5
6
7
8
9
function Person(name) {
this.name = name;
this.age = 18;
this.sayName = function() {
console.log(this.name);
}
}
// 第二�?创建实例
var person = new Person('person')

根据代码,我们可以得到下�?

下面分析一下:

  • 构造函数Person存在原型对象Person.prototype
  • 构造函数生成实例对象personperson__proto__指向构造函数Person原型对象
  • Person.prototype.__proto__ 指向内置对象,因�?Person.prototype 是个对象,默认是�?Object 函数作为类创建的,�?Object.prototype 为内置对�?
  • Person.__proto__ 指向内置匿名函数 anonymous,因�?Person 是个函数对象,默认由 Function 作为类创�?
  • Function.prototype �?Function.__proto__ 同时指向内置匿名函数 anonymous,这样原型链的终点就�?null

三、总结

下面首先要看几个概念�?
__proto__作为不同对象之间的桥梁,用来指向创建它的构造函数的原型对象�?

每个对象的__proto__都是指向它的构造函数的原型对象prototype�?

1
person1.__proto__ === Person.prototype

构造函数是一个函数对象,是通过 Function 构造器产生�?

1
Person.__proto__ === Function.prototype

原型对象本身是一个普通对象,而普通对象的构造函数都是Object

1
Person.prototype.__proto__ === Object.prototype

刚刚上面说了,所有的构造器都是函数对象,函数对象都�?Function 构造产生的

1
Object.__proto__ === Function.prototype

Object 的原型对象也有__proto__属性指向nullnull是原型链的顶�?

1
Object.prototype.__proto__ === null

下面作出总结�?

  • 一切对象都是继承自Object对象,Object 对象直接继承根源对象 null

  • 一切的函数对象(包�?Object 对象),都是继承�?Function 对象

  • Object 对象直接继承�?Function 对象

  • Function对象的__proto__会指向自己的原型对象,最终还是继承自Object对象

参考文�?

面试官:如何实现上拉加载,下拉刷新?

一、前言

下拉刷新和上拉加载这两种交互方式通常出现在移动端�?
本质上等同于PC网页中的分页,只是交互形式不�?
开源社区也有很多优秀的解决方案,如iscrollbetter-scrollpulltorefresh.js库等�?
这些第三方库使用起来非常便捷

我们通过原生的方式实现一次上拉加载,下拉刷新,有助于对第三方库有更好的理解与使用

二、实现原�?

上拉加载及下拉刷新都依赖于用户交�?
最重要的是要理解在什么场景,什么时机下触发交互动作

上拉加载

首先可以看一张图

上拉加载的本质是页面触底,或者快要触底时的动�?
判断页面触底我们需要先了解一下下面几个属�?

  • scrollTop:滚动视窗的高度距离window顶部的距离,它会随着往上滚动而不断增加,初始值是0,它是一个变化的�?
  • clientHeight:它是一个定值,表示屏幕可视区域的高度;
  • scrollHeight:页面不能滚动时也是存在�?此时scrollHeight等于clientHeight。scrollHeight表示body所有元素的总长�?包括body元素自身的padding)

综上我们得出一个触底公式:

1
scrollTop + clientHeight >= scrollHeight

简单实�?```js
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(“开始加载数�?);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22


### 下拉刷新
下拉刷新的本质是页面本身置于顶部时,用户下拉时需要触发的动作

关于下拉刷新的原生实现,主要分成三步�?
- 监听原生`touchstart`事件,记录其初始位置的值,`e.touches[0].pageY`�?- 监听原生`touchmove`事件,记录并计算当前滑动的位置值与初始位置值的差值,大于`0`表示向下拉动,并借助CSS3的`translateY`属性使元素跟随手势向下滑动对应的差值,同时也应设置一个允许滑动的最大值;
- 监听原生`touchend`事件,若此时元素滑动达到最大值,则触发`callback`,同时将`translateY`重设为`0`,元素回到初始位�?
举个例子�?
`Html`结构如下�?
```js
<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事件,记录初始的�?

1
2
3
4
5
6
7
8
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移动事件,记录滑动差�?

1
2
3
4
5
6
7
8
9
10
11
12
_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离开的事�?

1
2
3
4
5
6
7
_element.addEventListener('touchend', function(e) {
_element.style.transition = 'transform 0.5s ease 1s';
_element.style.transform = 'translateY(0px)';
_refreshText.innerText = '更新�?..';
// todo...

}, false);

从上面可以看到,在下拉到松手的过程中,经历了三个阶段�?

  • 当前手势滑动位置与初始位置差值大于零时,提示正在进行下拉刷新操作
  • 下拉到一定值时,显示松手释放后的操作提�?- 下拉到达设定最大值松手时,执行回调,提示正在进行更新操作

三、案�?

在实际开发中,我们更多的是使用第三方库,下面以better-scroll进行举例�?
HTML结构

1
2
3
4
5
6
7
8
9
<div id="position-wrapper">
<div>
<p class="refresh">下拉刷新</p >
<div class="position-list">
<!--列表内容-->
</div>
<p class="more">查看更多</p >
</div>
</div>

实例化上拉下拉插件,通过use来注册插�?

1
2
3
4
5
import BScroll from "@better-scroll/core";
import PullDown from "@better-scroll/pull-down";
import PullUp from '@better-scroll/pull-up';
BScroll.use(PullDown);
BScroll.use(PullUp);

实例化BetterScroll,并传入相关的参�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
let pageNo = 1,pageSize = 10,dataList = [],isMore = true;  
var scroll= new BScroll("#position-wrapper",{
scrollY:true,//垂直方向滚动
click:true,//默认会阻止浏览器的原生click事件,如果需要点击,这里要设为true
pullUpLoad:true,//上拉加载更多
pullDownRefresh:{
threshold:50,//触发pullingDown事件的位�? stop:0//下拉回弹后停留的位置
}
});
//监听下拉刷新
scroll.on("pullingDown",pullingDownHandler);
//监测实时滚动
scroll.on("scroll",scrollHandler);
//上拉加载更多
scroll.on("pullingUp",pullingUpHandler);

async function pullingDownHandler(){
dataList=[];
pageNo=1;
isMore=true;
$(".more").text("查看更多");
await getlist();//请求数据
scroll.finishPullDown();//每次下拉结束后,需要执行这个操�? scroll.refresh();//当滚动区域的dom结构有变化时,需要执行这个操�?}
async function pullingUpHandler(){
if(!isMore){
$(".more").text("没有更多数据�?);
scroll.finishPullUp();//每次上拉结束后,需要执行这个操�? return;
}
pageNo++;
await this.getlist();//请求数据
scroll.finishPullUp();//每次上拉结束后,需要执行这个操�? scroll.refresh();//当滚动区域的dom结构有变化时,需要执行这个操�?
}
function scrollHandler(){
if(this.y>50) $('.refresh').text("松手开始加�?);
else $('.refresh').text("下拉刷新");
}
function getlist(){
//返回的数�? let result=....;
dataList=dataList.concat(result);
//判断是否已加载完
if(result.length<pageSize) isMore=false;
//将dataList渲染到html内容�?}

注意点:

使用better-scroll 实现下拉刷新、上拉加载时要注意以下几点:

  • wrapper里必须只有一个子元素
  • 子元素的高度要比wrapper要高
  • 使用的时候,要确定DOM元素是否已经生成,必须要等到DOM渲染完成后,再new BScroll()
  • 滚动区域的DOM元素结构有变化后,需要执行刷�?refresh()
  • 上拉或者下拉,结束后,需要执行finishPullUp()或者finishPullDown(),否则将不会执行下次操作
  • better-scroll,默认会阻止浏览器的原生click事件,如果滚动内容区要添加点击事件,需要在实例化属性里设置click:true

小结

下拉刷新、上拉加载原理本身都很简单,真正复杂的是封装过程中,要考虑的兼容性、易用性、性能等诸多细�?

参考文�?

面试官:说说你对正则表达式的理解?应用场景?

一、是什�?

正则表达式是一种用来匹配字符串的强有力的武�?
它的设计思想是用一种描述性的语言定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了,否则,该字符串就是不合法的

�?JavaScript中,正则表达式也是对象,构建正则表达式有两种方式�?

  1. 字面量创建,其由包含在斜杠之间的模式组成
1
const re = /\d+/g;
  1. 调用RegExp对象的构造函�?
    1
    2
    3
    4
    const re = new RegExp("\\d+","g");

    const rul = "\\d+"
    const re1 = new RegExp(rul,"g");

使用构建函数创建,第一个参数可以是一个变量,遇到特殊字符\需要使用\\进行转义

二、匹配规�?

常见的校验规则如下:

规则 描述
\ 转义
^ 匹配输入的开�?
$ 匹配输入的结�?
* 匹配前一个表达式 0 次或多次
+ 匹配前面一个表达式 1 次或者多次。等价于 {1,}
? 匹配前面一个表达式 0 次或�?1 次。等价于{0,1}
. 默认匹配除换行符之外的任何单个字�?
x(?=y) 匹配’x’仅仅�?x’后面跟着’y’。这种叫做先行断言
(?<=y)x 匹配’x’仅当’x’前面�?y’.这种叫做后行断言
x(?!y) 仅仅�?x’后面不跟着’y’时匹�?x’,这被称为正向否定查�?
(?<!y)x 仅仅�?x’前面不是’y’时匹�?x’,这被称为反向否定查�?
x|y 匹配‘x’或者‘y�?
{n} n 是一个正整数,匹配了前面一个字符刚好出现了 n �?
{n,} n是一个正整数,匹配前一个字符至少出现了n�?
{n,m} n �?m 都是整数。匹配前面的字符至少n次,最多m�?
[xyz] 一个字符集合。匹配方括号中的任意字符
[^xyz] 匹配任何没有包含在方括号中的字符
\b 匹配一个词的边界,例如在字母和空格之间
\B 匹配一个非单词边界
\d 匹配一个数�?
\D 匹配一个非数字字符
\f 匹配一个换页符
\n 匹配一个换行符
\r 匹配一个回车符
\s 匹配一个空白字符,包括空格、制表符、换页符和换行符
\S 匹配一个非空白字符
\w 匹配一个单字字符(字母、数字或者下划线�?
\W 匹配一个非单字字符

正则表达式标�?

标志 描述
g 全局搜索�?
i 不区分大小写搜索�?
m 多行搜索�?
s 允许 . 匹配换行符�?
u 使用unicode码的模式进行匹配�?
y 执行“粘�?sticky)”搜�?匹配从目标字符串的当前位置开始�?

使用方法如下�?

1
2
var re = /pattern/flags;
var re = new RegExp("pattern", "flags");

在了解下正则表达式基本的之外,还可以掌握几个正则表达式的特性:

贪婪模式

在了解贪婪模式前,首先举个例子:

1
const reg = /ab{1,3}c/

在匹配过程中,尝试可能的顺序是从多往少的方向去尝试。首先会尝试bbb,然后再看整个正则是否能匹配。不能匹配时,吐出一个b,即在bb的基础上,再继续尝试,以此重复

如果多个贪婪量词挨着,则深度优先搜索

1
2
3
4
const string = "12345";
const regx = /(\d{1,3})(\d{1,3})/;
console.log( string.match(reg) );
// => ["12345", "123", "45", index: 0, input: "12345"]

其中,前面的\d{1,3}匹配的是”123”,后面的\d{1,3}匹配的是”45”

懒惰模式

惰性量词就是在贪婪量词后面加个问号。表示尽可能少的匹配

1
2
3
4
var string = "12345";
var regex = /(\d{1,3}?)(\d{1,3})/;
console.log( string.match(regex) );
// => ["1234", "1", "234", index: 0, input: "12345"]

其中\d{1,3}?只匹配到一个字�?1”,而后面的\d{1,3}匹配�?234”

分组

分组主要是用过()进行实现,比如beyond{3},是匹配d字母3次。而(beyond){3}是匹配beyond三次

()内使用|达到或的效果,如(abc | xxx)可以匹配abc或者xxx

反向引用,巧用$分组捕获

1
2
3
let str = "John Smith";

// 交换名字和姓�?console.log(str.replace(/(john) (smith)/i, '$2, $1')) // Smith, John

三、匹配方�?

正则表达式常被用于某些方法,我们可以分成两类�?

  • 字符串(str)方法:matchmatchAllsearchreplacesplit
  • 正则对象下(regexp)的方法:testexec
方法 描述
exec 一个在字符串中执行查找匹配的RegExp方法,它返回一个数组(未匹配到则返�?null)�?
test 一个在字符串中测试是否匹配的RegExp方法,它返回 true �?false�?
match 一个在字符串中执行查找匹配的String方法,它返回一个数组,在未匹配到时会返�?null�?
matchAll 一个在字符串中执行查找所有匹配的String方法,它返回一个迭代器(iterator)�?
search 一个在字符串中测试匹配的String方法,它返回匹配到的位置索引,或者在失败时返�?1�?
replace 一个在字符串中执行查找匹配的String方法,并且使用替换字符串替换掉匹配到的子字符串�?
split 一个使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中的 String 方法�?

str.match(regexp)

str.match(regexp) 方法在字符串 str 中找到匹�?regexp 的字�?
如果 regexp 不带�?g 标记,则它以数组的形式返回第一个匹配项,其中包含分组和属�?index(匹配项的位置)、input(输入字符串,等�?str�?

1
2
3
4
5
6
7
8
9
10
let str = "I love JavaScript";

let result = str.match(/Java(Script)/);

console.log( result[0] ); // JavaScript(完全匹配)
console.log( result[1] ); // Script(第一个分组)
console.log( result.length ); // 2

// 其他信息�?console.log( result.index ); // 7(匹配位置)
console.log( result.input ); // I love JavaScript(源字符串)

如果 regexp 带有 g 标记,则它将所有匹配项的数组作为字符串返回,而不包含分组和其他详细信�?

1
2
3
4
5
6
let str = "I love JavaScript";

let result = str.match(/Java(Script)/g);

console.log( result[0] ); // JavaScript
console.log( result.length ); // 1

如果没有匹配项,则无论是否带有标�?g ,都将返�?null

1
2
3
4
5
let str = "I love JavaScript";

let result = str.match(/HTML/);

console.log(result); // null

str.matchAll(regexp)

返回一个包含所有匹配正则表达式的结果及分组捕获组的迭代�?

1
2
3
4
5
6
7
8
9
10
const regexp = /t(e)(st(\d?))/g;
const str = 'test1test2';

const array = [...str.matchAll(regexp)];

console.log(array[0]);
// expected output: Array ["test1", "e", "st1", "1"]

console.log(array[1]);
// expected output: Array ["test2", "e", "st2", "2"]

str.search(regexp)

返回第一个匹配项的位置,如果未找到,则返�?-1

1
2
3
let str = "A drop of ink may make a million think";

console.log( str.search( /ink/i ) ); // 10(第一个匹配位置)

这里需要注意的是,search 仅查找第一个匹配项

str.replace(regexp)

替换与正则表达式匹配的子串,并返回替换后的字符串。在不设置全局匹配g的时候,只替换第一个匹配成功的字符串片�?

1
2
3
4
5
6
const reg1=/javascript/i;
const reg2=/javascript/ig;
console.log('hello Javascript Javascript Javascript'.replace(reg1,'js'));
//hello js Javascript Javascript
console.log('hello Javascript Javascript Javascript'.replace(reg2,'js'));
//hello js js js

str.split(regexp)

使用正则表达式(或子字符串)作为分隔符来分割字符�?

1
console.log('12, 34, 56'.split(/,\s*/)) // 数组 ['12', '34', '56']

regexp.exec(str)

regexp.exec(str) 方法返回字符�?str 中的 regexp 匹配项,与以前的方法不同,它是在正则表达式而不是字符串上调用的

根据正则表达式是否带有标�?g,它的行为有所不同

如果没有 g,那�?regexp.exec(str) 返回的第一个匹配与 str.match(regexp) 完全相同

如果有标�?g,调�?regexp.exec(str) 会返回第一个匹配项,并将紧随其后的位置保存在属性regexp.lastIndex 中�?下一次同样的调用会从位置 regexp.lastIndex 开始搜索,返回下一个匹配项,并将其后的位置保存�?regexp.lastIndex �?

1
2
3
4
5
6
7
8
9
10
let str = 'More about JavaScript at https://javascript.info';
let regexp = /javascript/ig;

let result;

while (result = regexp.exec(str)) {
console.log( `Found ${result[0]} at position ${result.index}` );
// Found JavaScript at position 11
// Found javascript at position 33
}

regexp.test(str)

查找匹配项,然后返回 true/false 表示是否存在

1
2
3
let str = "I love JavaScript";

// 这两个测试相�?console.log( /love/i.test(str) ); // true

四、应用场�?

通过上面的学习,我们对正则表达式有了一定的了解

下面再来看看正则表达式一些案例场景:

验证QQ合法性(5~15位、全是数字、不�?开头)�?

1
2
const reg = /^[1-9][0-9]{4,14}$/
const isvalid = patrn.exec(s)

校验用户账号合法性(只能输入5-20个以字母开头、可带数字、“_”、�?”的字串):

1
2
var patrn=/^[a-zA-Z]{1}([a-zA-Z0-9]|[._]){4,19}$/;
const isvalid = patrn.exec(s)

url参数解析为对�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const protocol = '(?<protocol>https?:)';
const host = '(?<host>(?<hostname>[^/#?:]+)(?::(?<port>\\d+))?)';
const path = '(?<pathname>(?:\\/[^/#?]+)*\\/?)';
const search = '(?<search>(?:\\?[^#]*)?)';
const hash = '(?<hash>(?:#.*)?)';
const reg = new RegExp(`^${protocol}\/\/${host}${path}${search}${hash}$`);
function execURL(url){
const result = reg.exec(url);
if(result){
result.groups.port = result.groups.port || '';
return result.groups;
}
return {
protocol:'',host:'',hostname:'',port:'',
pathname:'',search:'',hash:'',
};
}

console.log(execURL('https://localhost:8080/?a=b#xxxx'));
protocol: "https:"
host: "localhost:8080"
hostname: "localhost"
port: "8080"
pathname: "/"
search: "?a=b"
hash: "#xxxx"

再将上面的searchhash进行解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function execUrlParams(str){
str = str.replace(/^[#?&]/,'');
const result = {};
if(!str){ //如果正则可能配到空字符串,极有可能造成死循环,判断很重�? return result;
}
const reg = /(?:^|&)([^&=]*)=?([^&]*?)(?=&|$)/y
let exec = reg.exec(str);
while(exec){
result[exec[1]] = exec[2];
exec = reg.exec(str);
}
return result;
}
console.log(execUrlParams('#'));// {}
console.log(execUrlParams('##'));//{'#':''}
console.log(execUrlParams('?q=3606&src=srp')); //{q: "3606", src: "srp"}
console.log(execUrlParams('test=a=b=c&&==&a='));//{test: "a=b=c", "": "=", a: ""}

参考文�?

面试官:说说你对作用域链的理�?

一、作用域

作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合

换句话说,作用域决定了代码区块中变量和其他资源的可见�?
举个例子

1
2
3
4
5
function myFunction() {
let inVariable = "函数内部变量";
}
myFunction();//要先执行这个函数,否则根本不知道里面是啥
console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined

上述例子中,函数myFunction内部创建一个inVariable变量,当我们在全局访问这个变量的时候,系统会报�?
这就说明我们在全局是无法获取到(闭包除外)函数内部的变�?

我们一般将作用域分成:

  • 全局作用�?- 函数作用�?
  • 块级作用�?

全局作用�?

任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访�?

1
2
3
4
5
6
7
// 全局变量
var greeting = 'Hello World!';
function greet() {
console.log(greeting);
}
// 打印 'Hello World!'
greet();

函数作用�?

函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访�?

1
2
3
4
5
6
7
8
function greet() {
var greeting = 'Hello World!';
console.log(greeting);
}
// 打印 'Hello World!'
greet();
// 报错�?Uncaught ReferenceError: greeting is not defined
console.log(greeting);

可见上述代码中在函数内部声明的变量或函数,在函数外部是无法访问的,这说明在函数内部定义的变量或者方法只是函数作用域

块级作用�?

ES6引入了letconst关键�?和var关键字不同,在大括号中使用letconst声明的变量存在于块级作用域中。在大括号之外不能访问这些变�?

1
2
3
4
5
6
7
8
9
{
// 块级作用域中的变�? let greeting = 'Hello World!';
var lang = 'English';
console.log(greeting); // Prints 'Hello World!'
}
// 变量 'English'
console.log(lang);
// 报错:Uncaught ReferenceError: greeting is not defined
console.log(greeting);

二、词法作用域

词法作用域,又叫静态作用域,变量被创建时就确定好了,而非执行阶段确定的。也就是说我们写好代码时它的作用域就确定了,JavaScript 遵循的就是词法作用域

1
2
3
4
5
6
7
8
9
var a = 2;
function foo(){
console.log(a)
}
function bar(){
var a = 3;
foo();
}
bar()

上述代码改变成一张图

由于JavaScript遵循词法作用域,相同层级�?foo �?bar 就没有办法访问到彼此块作用域中的变量,所以输�?

三、作用域�?

当在Javascript中使用一个变量的时候,首先Javascript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用�?
如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错

这里拿《你不知道的Javascript(�?》中的一张图解释�?
把作用域比喻成一个建筑,这份建筑代表程序中的嵌套作用域链,第一层代表当前的执行作用域,顶层代表全局作用�?

变量的引用会顺着当前楼层进行查找,如果找不到,则会往上一层找,一旦到达顶层,查找的过程都会停�?
下面代码演示下:

1
2
3
4
5
6
7
8
9
10
11
12
var sex = '�?;
function person() {
var name = '张三';
function student() {
var age = 18;
console.log(name); // 张三
console.log(sex); // �?
}
student();
console.log(age); // Uncaught ReferenceError: age is not defined
}
person();

上述代码主要主要做了以下工作�?

  • student函数内部属于最内层作用域,找不到name,向上一层作用域person函数内部找,找到了输出“张三�?- student内部输出sex时找不到,向上一层作用域person函数找,还找不到继续向上一层找,即全局作用域,找到了输出“男�?- 在person函数内部输出age时找不到,向上一层作用域找,即全局作用域,还是找不到则报错

面试官:web常见的攻击方式有哪些?如何防御?

一、是什�?Web攻击(WebAttack)是针对用户上网行为或网站服务器等设备进行攻击的行为

如植入恶意代码,修改网站权限,获取网站用户隐私信息等�?
Web应用程序的安全性是任何基于Web业务的重要组成部�?
确保Web应用程序安全十分重要,即使是代码中很小的 bug 也有可能导致隐私信息被泄�?
站点安全就是为保护站点不受未授权的访问、使用、修改和破坏而采取的行为或实�?
我们常见的Web攻击方式�?- XSS (Cross Site Scripting) 跨站脚本攻击

  • CSRF(Cross-site request forgery)跨站请求伪�?- SQL注入攻击

二、XSS

XSS,跨站脚本攻击,允许攻击者将恶意代码植入到提供给其它用户使用的页面中

XSS涉及到三方,即攻击者、客户端与Web应用

XSS的攻击目标是为了盗取存储在客户端的cookie或者其他网站用于识别客户端身份的敏感信息。一旦获取到合法用户的信息后,攻击者甚至可以假冒合法用户与网站进行交互

举个例子�?
一个搜索页面,根据url参数决定关键词的内容

1
2
3
4
5
<input type="text" value="<%= getParameter("keyword") %>">
<button>搜索</button>
<div>
您搜索的关键词是�?%= getParameter("keyword") %>
</div>

这里看似并没有问题,但是如果不按套路出牌呢?

用户输入"><script>alert('XSS');</script>,拼接到 HTML 中返回给浏览器。形成了如下�?HTML�?

1
2
3
4
5
<input type="text" value=""><script>alert('XSS');</script>">
<button>搜索</button>
<div>
您搜索的关键词是�?><script>alert('XSS');</script>
</div>

浏览器无法分辨出 <script>alert('XSS');</script> 是恶意代码,因而将其执行,试想一下,如果是获取cookie发送对黑客服务器呢�?
根据攻击的来源,XSS攻击可以分成�?

  • 存储�?- 反射�?- DOM �?

存储�?

存储�?XSS 的攻击步骤:

  1. 攻击者将恶意代码提交到目标网站的数据库中
  2. 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览�?3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执�?4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作

这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等

反射�?XSS

反射�?XSS 的攻击步骤:

  1. 攻击者构造出特殊�?URL,其中包含恶意代�?2. 用户打开带有恶意代码�?URL 时,网站服务端将恶意代码�?URL 中取出,拼接�?HTML 中返回给浏览�?3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执�?4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作

反射�?XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库里,反射�?XSS 的恶意代码存�?URL 里�?
反射�?XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等�?
由于需要用户主动打开恶意�?URL 才能生效,攻击者往往会结合多种手段诱导用户点击�?
POST 的内容也可以触发反射�?XSS,只不过其触发条件比较苛刻(需要构造表单提交页面,并引导用户点击),所以非常少�?

DOM �?XSS

DOM �?XSS 的攻击步骤:

  1. 攻击者构造出特殊�?URL,其中包含恶意代�?2. 用户打开带有恶意代码�?URL
  2. 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执�?4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作

DOM �?XSS 跟前两种 XSS 的区别:DOM �?XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前�?JavaScript 自身的安全漏洞,而其他两�?XSS 都属于服务端的安全漏�?

XSS的预�?

通过前面介绍,看到XSS攻击的两大要素:

  • 攻击者提交而恶意代�?- 浏览器执行恶意代�?
    针对第一个要素,我们在用户输入的过程中,过滤掉用户输入的恶劣代码,然后提交给后端,但是如果攻击者绕开前端请求,直接构造请求就不能预防�?
    而如果在后端写入数据库前,对输入进行过滤,然后把内容给前端,但是这个内容在不同地方就会有不同显示

例如�?
一个正常的用户输入�?5 < 7 这个内容,在写入数据库前,被转义,变成了 5 < 7

在客户端中,一旦经过了 escapeHTML(),客户端显示的内容就变成了乱�? 5 < 7 )

在前端中,不同的位置所需的编码也不同�?

  • �?5 < 7 作为 HTML 拼接页面时,可以正常显示�?

    1
    <div title="comment">5 &lt; 7</div>
  • �?5 < 7 通过 Ajax 返回,然后赋值给 JavaScript 的变量时,前端得到的字符串就是转义后的字符。这个内容不能直接用�?Vue 等模板的展示,也不能直接用于内容长度计算。不能用于标题、alert �?

可以看到,过滤并非可靠的,下面就要通过防止浏览器执行恶意代码:

在使�?.innerHTML.outerHTMLdocument.write() 时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 .textContent.setAttribute() �?
如果�?Vue/React 技术栈,并且不使用 v-html/dangerouslySetInnerHTML 功能,就在前�?render 阶段避免 innerHTMLouterHTML �?XSS 隐患

DOM 中的内联事件监听器,�?locationonclickonerroronloadonmouseover 等,<a> 标签�?href 属性,JavaScript �?eval()setTimeout()setInterval() 等,都能把字符串作为代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免

1
2
3
4
5
6
7
8
9
10
11
<!-- 链接内包含恶意代�?-->
< a href=" ">1</ a>

<script>
// setTimeout()/setInterval() 中调用恶意代�?setTimeout("UNTRUSTED")
setInterval("UNTRUSTED")

// location 调用恶意代码
location.href = 'UNTRUSTED'

// eval() 中调用恶意代�?eval("UNTRUSTED")

三、CSRF

CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请�?
利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目

一个典型的CSRF攻击有着如下的流程:

  • 受害者登录a.com,并保留了登录凭证(Cookie�?- 攻击者引诱受害者访问了b.com
  • b.com �?a.com 发送了一个请求:a.com/act=xx。浏览器会默认携带a.com的Cookie
  • a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求
  • a.com以受害者的名义执行了act=xx
  • 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作

csrf可以通过get请求,即通过访问img的页面后,浏览器自动访问目标地址,发送请�?
同样,也可以设置一个自动提交的表单发送post请求,如下:

1
2
3
4
5
6
<form action="http://bank.example/withdraw" method=POST>
<input type="hidden" name="account" value="xiaoming" />
<input type="hidden" name="amount" value="10000" />
<input type="hidden" name="for" value="hacker" />
</form>
<script> document.forms[0].submit(); </script>

访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作

还有一种为使用a标签的,需要用户点击链接才会触�?
访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作

1
2
3
< a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">
重磅消息!!
<a/>

CSRF的特�?

  • 攻击一般发起在第三方网站,而不是被攻击的网站。被攻击的网站无法防止攻击发�?- 攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作;而不是直接窃取数�?- 整个过程攻击者并不能获取到受害者的登录凭证,仅仅是“冒用�?- 跨站请求可以用各种方式:图片URL、超链接、CORS、Form提交等等。部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追�?

CSRF的预�?

CSRF通常从第三方网站发起,被攻击的网站无法防止攻击发生,只能通过增强自己网站针对CSRF的防护能力来提升安全�?
防止csrf常用方案如下�?

  • 阻止不明外域的访�? - 同源检�? - Samesite Cookie
  • 提交时要求附加本域才能获取的信息
    • CSRF Token
    • 双重Cookie验证

这里主要讲讲token这种形式,流程如下:

  • 用户打开页面的时候,服务器需要给这个用户生成一个Token

  • 对于GET请求,Token将附在请求地址之后。对�?POST 请求来说,要�?form 的最后加�?

    1
    <input type=”hidden�?name=”csrftoken�?value=”tokenvalue�?>
  • 当用户从客户端得到了Token,再次提交给服务器的时候,服务器需要判断Token的有效�?

四、SQL注入

Sql 注入攻击,是通过将恶意的 Sql 查询或添加语句插入到应用的输入参数中,再在后�?Sql 服务器上解析执行进行的攻�?

流程如下所示:

  • 找出SQL漏洞的注入点

  • 判断数据库的类型以及版本

  • 猜解用户名和密码

  • 利用工具查找Web后台管理入口

  • 入侵和破�?
    预防方式如下�?

  • 严格检查输入变量的类型和格�?- 过滤和转义特殊字�?- 对访问数据库的Web应用程序采用Web应用防火�?
    上述只是列举了常见的web攻击方式,实际开发过程中还会遇到很多安全问题,对于这些问题, 切记不可忽视

参考文�?

面试官:什么是单点登录?如何实现?

一、是什�?

单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一

SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统

SSO 一般都需要一个独立的认证中心(passport),子系统的登录均得通过passport,子系统本身将不参与登录操作

当一个系统成功登录以后,passport将会颁发一个令牌给各个子系统,子系统可以拿着令牌会获取各自的受保护资源,为了减少频繁认证,各个子系统在被passport授权以后,会建立一个局部会话,在一定时间内可以无需再次向passport发起认证

上图有四个系统,分别是Application1Application2Application3、和SSO,当Application1Application2Application3需要登录时,将跳到SSO系统,SSO系统完成登录,其他的应用系统也就随之登录�?

举个例子

淘宝、天猫都属于阿里旗下,当用户登录淘宝后,再打开天猫,系统便自动帮用户登录了天猫,这种现象就属于单点登录

二、如何实�?

同域名下的单点登�?

cookiedomain属性设置为当前域的父域,并且父域的cookie会被子域所共享。path属性默认为web应用的上下文路径

利用 Cookie 的这个特点,没错,我们只需要将Cookie domain属性设置为父域的域名(主域名),同时将 Cookie path属性设置为根路径,�?Session ID(或 Token)保存到父域中。这样所有的子域应用就都可以访问到这个Cookie

不过这要求应用系统的域名需建立在一个共同的主域名之下,�?tieba.baidu.com �?map.baidu.com,它们都建立�?baidu.com 这个主域名之下,那么它们就可以通过这种方式来实现单点登�?

不同域名下的单点登录(一)

如果是不同域的情况下,Cookie是不共享的,这里我们可以部署一个认证中心,用于专门处理登录请求的独立的 Web 服务

用户统一在认证中心进行登录,登录成功后,认证中心记录用户的登录状态,并将 token 写入 Cookie(注意这�?Cookie 是认证中心的,应用系统是访问不到的)

应用系统检查当前请求有没有 Token,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心

由于这个操作会将认证中心�?Cookie 自动带过去,因此,认证中心能够根�?Cookie 知道用户是否已经登录过了

如果认证中心发现用户尚未登录,则返回登录页面,等待用户登�?
如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标 URL ,并在跳转前生成一�?Token,拼接在目标 URL 的后面,回传给目标应用系�?
应用系统拿到 Token 之后,还需要向认证中心确认�?Token 的合法性,防止用户伪造。确认无误后,应用系统记录用户的登录状态,并将 Token 写入 Cookie,然后给本次访问放行。(注意这个 Cookie 是当前应用系统的)当用户再次访问当前应用系统时,就会自动带上这个 Token,应用系统验�?Token 发现用户已登录,于是就不会有认证中心什么事�?
此种实现方式相对复杂,支持跨域,扩展性好,是单点登录的标准做�?

不同域名下的单点登录(�?

可以选择�?Session ID (或 Token )保存到浏览器的 LocalStorage 中,让前端在每次向后端发送请求时,主动将LocalStorage的数据传递给服务�?
这些都是由前端来控制的,后端需要做的仅仅是在用户登录成功后,将 Session ID (或 Token )放在响应体中传递给前端

单点登录完全可以在前端实现。前端拿�?Session ID (或 Token )后,除了将它写入自己的 LocalStorage 中之外,还可以通过特殊手段将它写入多个其他域下�?LocalStorage �?
关键代码如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 获取 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 被多个域所共享

此种实现方式完全由前端控制,几乎不需要后端参与,同样支持跨域

三、流�?

单点登录的流程图如下所示:

  • 用户访问系统1的受保护资源,系�?发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数

  • sso认证中心发现用户未登录,将用户引导至登录页面

  • 用户输入用户名密码提交登录申�?- sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令�?- sso认证中心带着令牌跳转会最初的请求地址(系�?�?- 系统1拿到令牌,去sso认证中心校验令牌是否有效

  • sso认证中心校验令牌,返回有效,注册系统1

  • 系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资�?- 用户访问系统2的受保护资源

  • 系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数

  • sso认证中心发现用户已登录,跳转回系�?的地址,并附上令牌

  • 系统2拿到令牌,去sso认证中心校验令牌是否有效

  • sso认证中心校验令牌,返回有效,注册系统2

  • 系统2使用该令牌创建与用户的局部会话,返回受保护资�?
    用户登录成功之后,会与sso认证中心及各个子系统建立会话,用户与sso认证中心建立的会话称为全局会话

用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过sso认证中心

全局会话与局部会话有如下约束关系�?

  • 局部会话存在,全局会话一定存�?- 全局会话存在,局部会话不一定存�?- 全局会话销毁,局部会话必须销�?

参考文�?

面试官:JavaScript字符串的常用方法有哪些?

一、操作方�?

我们也可将字符串常用的操作方法归纳为增、删、改、查,需要知道字符串的特点是一旦创建了,就不可�?

�?

这里增的意思并不是说直接增添内容,而是创建字符串的一个副本,再进行操�?
除了常用+以及${}进行字符串拼接之外,还可通过concat

concat

用于将一个或多个字符串拼接成一个新字符�?

1
2
3
4
let stringValue = "hello ";
let result = stringValue.concat("world");
console.log(result); // "hello world"
console.log(stringValue); // "hello"

�?

这里的删的意思并不是说删除原字符串的内容,而是创建字符串的一个副本,再进行操�?
常见的有�?

  • slice()
  • substr()
  • substring()

这三个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数�?

1
2
3
4
5
6
7
let stringValue = "hello world";
console.log(stringValue.slice(3)); // "lo world"
console.log(stringValue.substring(3)); // "lo world"
console.log(stringValue.substr(3)); // "lo world"
console.log(stringValue.slice(3, 7)); // "lo w"
console.log(stringValue.substring(3,7)); // "lo w"
console.log(stringValue.substr(3, 7)); // "lo worl"

�?

这里改的意思也不是改变原字符串,而是创建字符串的一个副本,再进行操�?
常见的有�?

  • trim()、trimLeft()、trimRight()

  • repeat()

  • padStart()、padEnd()

  • toLowerCase()�?toUpperCase()

trim()、trimLeft()、trimRight()

删除前、后或前后所有空格符,再返回新的字符�?

1
2
3
4
let stringValue = " hello world ";
let trimmedStringValue = stringValue.trim();
console.log(stringValue); // " hello world "
console.log(trimmedStringValue); // "hello world"

repeat()

接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结�?

1
2
let stringValue = "na ";
let copyResult = stringValue.repeat(2) // na na

padEnd()

复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件

1
2
3
let stringValue = "foo";
console.log(stringValue.padStart(6)); // " foo"
console.log(stringValue.padStart(9, ".")); // "......foo"

toLowerCase()�?toUpperCase()

大小写转�?

1
2
3
let stringValue = "hello world";
console.log(stringValue.toUpperCase()); // "HELLO WORLD"
console.log(stringValue.toLowerCase()); // "hello world"

�?

除了通过索引的方式获取字符串的值,还可通过�?

  • chatAt()

  • indexOf()

  • startWith()

  • includes()

charAt()

返回给定索引位置的字符,由传给方法的整数参数指定

1
2
let message = "abcde";
console.log(message.charAt(2)); // "c"

indexOf()

从字符串开头去搜索传入的字符串,并返回位置(如果没找到,则返回 -1 �?

1
2
let stringValue = "hello world";
console.log(stringValue.indexOf("o")); // 4

startWith()、includes()

从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔�?

1
2
3
4
5
let message = "foobarbaz";
console.log(message.startsWith("foo")); // true
console.log(message.startsWith("bar")); // false
console.log(message.includes("bar")); // true
console.log(message.includes("qux")); // false

二、转换方�?

split

把字符串按照指定的分割符,拆分成数组中的每一�?

1
2
let str = "12+23+34"
let arr = str.split("+") // [12,23,34]

三、模板匹配方�?

针对正则表达式,字符串设计了几个方法�?

  • match()
  • search()
  • replace()

match()

接收一个参数,可以是一个正则表达式字符串,也可以是一个RegExp对象,返回数�?

1
2
3
4
let text = "cat, bat, sat, fat";
let pattern = /.at/;
let matches = text.match(pattern);
console.log(matches[0]); // "cat"

接收一个参数,可以是一个正则表达式字符串,也可以是一个RegExp对象,找到则返回匹配索引,否则返�?-1

1
2
3
let text = "cat, bat, sat, fat";
let pos = text.search(/at/);
console.log(pos); // 1

replace()

接收两个参数,第一个参数为匹配的内容,第二个参数为替换的元素(可用函数�?

1
2
3
let text = "cat, bat, sat, fat";
let result = text.replace("at", "ond");
console.log(result); // "cond, bat, sat, fat"

面试官:举例说明你对尾递归的理解,有哪些应用场�?

一、递归

递归(英语:Recursion�?
在数学与计算机科学中,是指在函数的定义中使用函数自身的方�?
在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数

其核心思想是把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解

一般来说,递归需要有边界条件、递归前进阶段和递归返回阶段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回

下面实现一个函�?pow(x, n),它可以计算 x �?n 次方

使用迭代的方式,如下�?

1
2
3
4
5
6
7
8
function pow(x, n) {
let result = 1;

// 再循环中,用 x 乘以 result n �? for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}

使用递归的方式,如下�?

1
2
3
4
5
6
7
function pow(x, n) {
if (n == 1) {
return x;
} else {
return x * pow(x, n - 1);
}
}

pow(x, n) 被调用时,执行分为两个分支:

1
2
3
4
5
             if n==1  = x
/
pow(x, n) =
\
else = x * pow(x, n - 1)

也就是说pow 递归地调用自�?直到 n == 1

为了计算 pow(2, 4),递归变体经过了下面几个步骤:

  1. pow(2, 4) = 2 * pow(2, 3)
  2. pow(2, 3) = 2 * pow(2, 2)
  3. pow(2, 2) = 2 * pow(2, 1)
  4. pow(2, 1) = 2

因此,递归将函数调用简化为一个更简单的函数调用,然后再将其简化为一个更简单的函数,以此类推,直到结果

二、尾递归

尾递归,即在函数尾位置调用自身(或是一个尾调用本身的其他函数等等)。尾递归也是递归的一种特殊情形。尾递归是一种特殊的尾调用,即在尾部直接调用自身的递归函数

尾递归在普通尾调用的基础上,多出�?个特征:

  • 在尾部调用的是函数自�?- 可通过优化,使得计算仅占用常量栈空�?
    在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储,递归次数过多容易造成栈溢�?
    这时候,我们就可以使用尾递归,即一个函数中所有递归形式的调用都出现在函数的末尾,对于尾递归来说,由于只存在一个调用记录,所以永远不会发�?栈溢�?错误

实现一下阶乘,如果用普通的递归,如下:

1
2
3
4
5
6
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}

factorial(5) // 120

如果n等于5,这个方法要执行5次,才返回最终的计算表达式,这样每次都要保存这个方法,就容易造成栈溢出,复杂度为O(n)

如果我们使用尾递归,则如下�?

1
2
3
4
5
6
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

可以看到,每一次返回的就是一个新的函数,不带上一个函数的参数,也就不需要储存上一个函数了。尾递归只需要保存一个调用栈,复杂度 O(1)

二、应用场�?

数组求和

1
2
3
4
5
6
function sumArray(arr, total) {
if(arr.length === 1) {
return total
}
return sum(arr, total + arr.pop())
}

使用尾递归优化求斐波那契数�?

1
2
3
4
5
6
function factorial2 (n, start = 1, total = 1) {
if(n <= 2){
return total
}
return factorial2 (n -1, total, total + start)
}

数组扁平�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let a = [1,2,3, [1,2,3, [1,2,3]]]
// 变成
let a = [1,2,3,1,2,3,1,2,3]
// 具体实现
function flat(arr = [], result = []) {
arr.forEach(v => {
if(Array.isArray(v)) {
result = result.concat(flat(v, []))
}else {
result.push(v)
}
})
return result
}

数组对象格式�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
let obj = {
a: '1',
b: {
c: '2',
D: {
E: '3'
}
}
}
// 转化为如下:
let obj = {
a: '1',
b: {
c: '2',
d: {
e: '3'
}
}
}

// 代码实现
function keysLower(obj) {
let reg = new RegExp("([A-Z]+)", "g");
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
let temp = obj[key];
if (reg.test(key.toString())) {
// 将修改后的属性名重新赋值给temp,并在对象obj内添加一个转换后的属�? temp = obj[key.replace(reg, function (result) {
return result.toLowerCase()
})] = obj[key];
// 将之前大写的键属性删�? delete obj[key];
}
// 如果属性是对象或者数组,重新执行函数
if (typeof temp === 'object' || Object.prototype.toString.call(temp) === '[object Array]') {
keysLower(temp);
}
}
}
return obj;
};

参考文�?

0%