JavaScript の面接の質問の概要

1. インタビュアー: JavaScript のデータ型について教えてください。収納の違い?

JavaScript では、基本型と複合型の 2 つの型に分類できます。

2 つのタイプの違いは、保存場所が異なることです。

基本的なタイプは次の 6 つです。

番号

ブール値

未定義

ヌル

シンボル

ES11 にはさらに多くの BigInt があり、メモリ内の固定サイズを占有し、スタック メモリに格納されます。

複雑なデータ型:

複合型はオブジェクトと総称され、主にオブジェクト配列関数が含まれます。

複合型の値は、オブジェクトがヒープ メモリに格納され、スタック メモリがオブジェクトの変数識別子とヒープ メモリ内のオブジェクトの格納アドレスを格納することです。

2. インタビュアー: 配列の一般的なメソッドは何ですか?

配列の基本操作は、追加、削除、変更、チェックに要約できます。

2.1増加

押す()

シフト解除()

スプライス()

concat()

配列の末尾に追加する

配列の先頭に追加します

3 つのパラメータ、開始位置、0、挿入要素

配列の末尾に追加する

配列の最新の長さを返します

配列の最新の長さを返します

空の配列を返します

新しく構築された配列を返します

2.2 削除

ポップ()

シフト()

スプライス()

スライス()

配列内の最後の項目を削除します

配列の最初の項目を削除します

2 つのパラメータ、開始位置、削除量

開始位置、終了位置

削除されたアイテムを返す

削除されたアイテムを返す

削除された要素を含む配列を返します

新しく構築された配列を返します

2.3 変更

スプライス()

開始位置、削除する要素の数、および挿入する任意の数の要素の 3 つのパラメータを渡し、削除された要素の配列を返します。これは元の配列に影響します。

2.4 チェック

の指標()

含む()

探す()

配列内の検索された要素の位置を返します。

配列内の検索された要素の位置を返します。

最初に一致した要素を返します

見つからない場合は -1 を返します

見つかった場合は true を返し、それ以外の場合は false を返します

配列のソート方法

逆行する()

選別()

配列要素の方向を反転する

並べ替え、比較関数の受け入れ、値がどのように並べ替えられるかを判断する

配列変換方法

加入()

join() メソッドは 1 つの引数、文字列区切り文字を受け取り、すべての項目を含む文字列を返します。

配列反復子メソッド

forEach()

地図()

フィルター()

いくつかの()

毎日()

配列を反復処理する

配列内の各項目は、渡された関数を実行します。

配列内の各項目は、渡された関数を実行します。

各アイテムは渡されたテスト関数を実行します

各アイテムは渡されたテスト関数を実行します

戻り値なし

呼び出し結果の配列を返します。

true の項目は配列を形成して返されます。

1 つの要素が true の場合に true を返します

すべての要素が true の場合は true を返します

3、面试官:谈谈 JavaScript 中的类型转换机制

JS中有六种简单数据类型和复杂数据类型,变量的数据类型在声明时是不确定的,但是各种运算符对数据类型是有要求的,如果运算子的类型与预期不符合,就会触发类型转换机制。

常见的类型转换有:显示转换和隐式转换

3.1 显示转换,即我们很清楚可以看到这里发生了类型的转变,常见的方法有

Number()

parseInt()

String()

Boolean()

3.2 隐式转换

1、在需要布尔值的地方,就会将非布尔值的参数自动转为布尔值,系统内部会调用Boolean函数

2、遇到预期为字符串的地方,就会将非字符串的值自动转为字符串,常发生在+运算中

3、除了+有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值

(null转为数值时,值为0 。undefined转为数值时,值为NaN)

4、面试官:== 和 ===区别,分别在什么情况使用

等于操作符用两个等于号( == )表示,如果操作数相等,则会返回 true

等于操作符(==)在比较中会先进行类型转换,再确定操作数是否相等

特殊例子:null 和undefined 相等 存在 NaN 则返回 false

全等操作符由 3 个等于号( === )表示,只有两个操作数在不转换的前提下相等才返回 true。即类型相同,值也需相同

案例:undefined 和 null与自身严格相等 null 和 undefined 比较为false

5、面试官:深拷贝浅拷贝的区别?如何实现一个深拷贝?

5.1 浅拷贝

如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址,即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址

下面简单实现一个浅拷贝

<body>
    <script>
        function shallowCopy(obj) {
            const newObj = {};
            //prop是属性名
            for (let prop in obj) {
            //hasOwnProperty() 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)。
                if (obj.hasOwnProperty(prop)) {    //
                    newObj[prop] = obj[prop];
                }
            }
            return newObj
        }

        var obj = {
            id: '1',
            name: 'andy'
        };
        console.log(shallowCopy(obj));
    </script>
</body>
newObj= Object.assign({ }, Obj); //语法糖,简单实现,不让手写时可以使用

还可以使用拓展运算符

5.2 深拷贝

深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性

常见的深拷贝方式有:

_.cloneDeep()

jQuery.extend()

JSON.stringify()

手写循环递归

1、const obj2 = _.cloneDeep(obj1)

2、obj2 = $.extend(true, {}, obj1)

3、obj2=JSON.parse(JSON.stringify(obj1));

  1. 手写深拷贝

<body>
    <script>
        let arr = [5, 4, 9, 8];
        let obj1 = {
            name: 'xxx',
            sex: '男',
            like: ['红色', '蓝色'],
            book: {
                title: 'js程序',
                price: '88'
            }
        };

        //手写深拷贝
        function deepClone(obj) {
            // 查看要拷贝的是数组还是对象,如果数组创建空数组,是对象创建空对象
            const newObj = obj instanceof Array ? [] : {};
            for (let k in obj) {
                //K属性名  obj[k]值
                //判断当前每个元素是否是对象或者数组 
                //如果还是对象,继续递归拷贝
                //是值,直接添加到新创建的对象或者数组里
                if (typeof obj[k] === 'object') {
                    //实现一个递归拷贝
                    newObj[k] = deepClone(obj[k])
                } else {    //否则是值, 直接添加到新建的 newObj中
                    newObj[k] = obj[k];
                }
            }   
            return newObj;
        }

        console.log(deepClone(obj1));
    </script>
</body>

6、面试官:说说你对闭包的理解?闭包使用场景

闭包指有权访问另一个函数作用域中变量的函数。

简单理解就是一个函数和对其周围状态的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)

闭包使用场景

1、创建私有变量 2、延长变量的生命周期

一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的

闭包柯里化

柯里化的目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用

假设我们有一个求长方形面积的函数,如果我们碰到的长方形的宽老是10,我们可以使用闭包柯里化这个计算面积的函数,之后碰到宽度为10的长方形就可以这样计算面积

而且如果遇到宽度偶尔变化也可以轻松复用

7、面试官:说说你对作用域链的理解

作用域,即变量和函数生效的区域或集合。作用域决定了代码区块中变量和其他资源的可见性

我们一般将作用域分成:1、全局作用域 2、函数作用域 3、块级作用域

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

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

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

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

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

说起原型链就要先说一下构造函数,什么是构造函数呢,它可以用来创建对象实例,当我们new了构造函数就会创建一个实例,但是构造函数有一个问题就是里面存放的函数方法每new一次就会多开辟一份内存空间,为了解决这个问题构造函数里面有一个属性叫prototype原型对象,专门存放公有的方法,实例对象怎么调用这个方法呢,因为所有对象身上都有对象原型__proto__,就可以通过它来调用,同时对象身上都有一个constructor 属性可以指回构造函数本身,原型链其实就是一个查找的过程,当我们想要查找对象实例身上的属性时先在本身找,找不到再用对象原型__proto__去原型对象prototype上找,再找不到再去原型对象的prototype上找,最终找不到返回最终指向null

9、面试官:Javascript如何实现继承?

继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码,在子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。使用extends 关键字实现类的继承。

classSon extends Father

常见的继承方式还有原型链继承、构造函数继承(借助 call调用Parent函数)。

10、面试官:谈谈this对象的理解

1、作为普通函数执行时,this指向window。

2、当函数作为对象的方法被调用时,this就会指向该对象。

3、构造器调用,this指向返回的这个对象。

4、箭头函数 箭头函数的this绑定看的是this所在函数定义在哪个对象下,就绑定哪个对象。如果有嵌套的情况,则this绑定到最近的一层对象上。

5、基于Function.prototype上的 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。apply接收参数的是数组,call接受参数列表,`` bind方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this指向除了使用new `时会被改变,其他情况下都不会改变。若为空默认是指向全局对象window。

11、面试官:JavaScript中执行上下文和执行栈是什么?

简单的来说,执行上下文是一种对Javascript代码执行环境的抽象概念,也就是说只要有Javascript代码运行,那么它就一定是运行在执行上下文中

执行上下文的类型分为三种:

1、全局执行上下文:只有一个,浏览器中的全局对象就是 window对象,this 指向这个全局对象

2、函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文

3、Eval函数执行上下文: 指的是运行在eval 函数中的代码,很少用而且不建议使用

执行上下文的生命周期包括三个阶段:创建阶段 → 执行阶段 → 回收阶段

创建阶段即当函数被调用,但未执行任何其内部代码之前,主要确定 this 的值

执行阶段:在这阶段,执行变量赋值、代码执行

如果 Javascript 引擎在源代码中声明的实际位置找不到变量的值,那么将为其分配 undefined 值

回收阶段:执行上下文出栈等待虚拟机回收执行上下文

二、执行栈

执行栈,也叫调用栈,具有后进先出结构,用于存储在代码执行期间创建的所有执行上下文

当Javascript引擎开始执行你第一行脚本代码的时候,它就会创建一个全局执行上下文然后将它压到执行栈中

每当引擎碰到一个函数的时候,它就会创建一个函数执行上下文,然后将这个执行上下文压到执行栈中

引擎会执行位于执行栈栈顶的执行上下文(一般是函数执行上下文),当该函数执行结束后,对应的执行上下文就会被弹出,然后控制流程到达执行栈的下一个执行上下文

12、面试官:说说JavaScript中的事件模型

一、事件与事件流

由于DOM是一个树结构,如果在父子节点绑定事件时候,当触发子节点的时候,就存在一个顺序问题,这就涉及到了事件流的概念,事件流都会经历三个阶段:

1、事件捕获阶段 2、处于目标阶段 3、事件冒泡阶段

事件冒泡是一种从下往上的传播方式,由最具体的元素(触发节点)然后逐渐向上传播到最不具体的那个节点,也就是DOM中最高层的父节点

事件捕获与事件冒泡相反,事件最开始由不太具体的节点最早接受事件, 而最具体的节点(触发节点)最后接受事件

二、事件模型

事件模型可以分为三种:

1、原始事件模型(DOM0级)2、标准事件模型(DOM2级)3、IE事件模型(基本不用)

原始事件模型:事件绑定监听函数比较简单,document.getElementById('.btn')

特性:绑定速度快、只支持冒泡,不支持捕获、一个类型的事件只能绑定一次

标准事件模型:事件绑定监听函数的方式 addEventListener(eventType, handler,useCapture)

特性:可以在一个DOM元素上绑定多个事件处理器,各自并不会冲突

执行时机:当第三个参数(useCapture)设置为true就在捕获过程中执行,反之在冒泡过程中执行处理函数

IE事件模型:attachEvent(eventType,handler)

13、面试官:typeof与 instanceof 区别

举个例子:

typeof 1     //          'number'
console.log(1instanceof Number);      // false

typeof与instanceof都是判断数据类型的方法,区别如下:

1、typeof会返回一个变量的基本类型,instanceof返回的是一个布尔值

2、instanceof 可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型

3、而typeof 也存在弊端,它虽然可以快速判断基础数据类型(null 除外),但是引用数据类型中,除了function 类型以外,其他的也无法判断

如果需要通用检测数据类型,可以采用Object.prototype.toString,调用该方法,统一返回格式“[object Xxx]”的字符串

14、面试官:解释下什么是事件代理?应用场景?

事件代理,简单来讲,就是把一个元素响应事件(click、keydown......)的函数委托到另一个元素,而事件委托在冒泡阶段完成。

当事件响应到目标元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数

应用场景:点击列表项的时候响应一个事件,可以把点击事件绑定在父级元素ul上面,然后执行事件的时候再去匹配目标元素

优点:1、减少整个页面所需的内存,提升整体性能 2、动态绑定,减少重复工作

但如focus、blur等事件没有事件冒泡机制,就无法进行事件委托。

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

1、首先创建了一个新的空对象

2、将对象与构建函数通过原型链连接起来

3、让函数的this指向这个对象,执行构造函数的代码(为这个新对象添加属性)

4、判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

16、面试官:ajax原理是什么?如何实现?

AJAX即异步的JavaScript 和XML,是一种创建交互式网页应用的网页开发技术,可以在不重新加载整个网页的情况下,与服务器交换数据,并且更新部分网页

Ajax的原理简单来说通过XmlHttpRequest对象来向服务器发异步请求,从服务器获得数据,然后用JavaScript来操作DOM而更新页面

实现过程

实现 Ajax异步交互需要服务器逻辑进行配合,需要完成以下步骤:

1、创建 Ajax的核心对象XMLHttpRequest对象

2、通过 XMLHttpRequest 对象的 open() 方法与服务端建立连接

3、构建请求所需的数据内容,并通过XMLHttpRequest 对象的 send() 方法发送给服务器端

4、通过 XMLHttpRequest 对象提供的 onreadystatechange 事件监听服务器端你的通信状态

5、接受并处理服务端向客户端响应的数据结果

6、将处理结果更新到 HTML页面中

17、面试官:bind、call、apply区别?

apply、call、bind三者的区别在于:

1、三者都可以改变函数的this对象指向

2、三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window

3、三者都可以传参,但是apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入

4、bind是返回绑定this之后的函数,不会立即执行,apply、call则是立即执行

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

正则表达式是一种用来匹配字符串的工具

它的设计思想是用一种描述性的语言定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了,否则,该字符串就是不合法的

它可由字面量或调用RegExp对象的构造函数来构建。

常见的校验规则如:

^匹配输入的开始

$匹配输入的结束

{n}匹配前面字符刚好出现了 n 次

g 全局搜索

应用场景如验证QQ合法性、校验用户账号合法性、将url参数解析为对象等

19、面试官:说说你对事件循环的理解

JS是单线程的,为了防止一个函数执行时间过长阻塞后面的代码,所以会先将同步代码压入执行栈中,依次执行,将异步代码推入异步队列,异步队列又分为宏任务队列和微任务队列,因为宏任务队列的执行时间较长,所以微任务队列要优先于宏任务队列。微任务队列的代表就是,Promise.then,MutationObserver,宏任务的话就是setImmediate setTimeout setInterval

当主程结束,先执行准备好微任务,然后再执行准备好的宏任务,一个轮询结束。

事件循环可以简单的描述为以下四个步骤:

1、函数入栈,当Stack中执行到异步任务的时候,就将他丢给WebAPIs,接着执行同步任务,直到Stack为空;

2、此期间WebAPIs完成这个事件,把回调函数放入队列中等待执行(微任务放到微任务队列,宏任务放到宏任务队列)

3、执行栈为空时,Event Loop把微任务队列执行清空;

4、微任务队列清空后,进入宏任务队列,取队列的第一项任务放入Stack(栈)中执行,执行完成后,查看微任务队列是否有任务,有的话,清空微任务队列。重复4,继续从宏任务中取任务执行,执行完成之后,继续清空微任务,如此反复循环,直至清空所有的任务。

20、面试官:DOM常见的操作有哪些?

DOM常见的操作,主要分为:

1、创建节点2、查询节点3、更新节点4、添加节点5、删除节点

创建节点

获取节点

更新节点

添加节点

删除节点

createElement

querySelector

innerHTML(危险)

innerText(安全)

appendChild,insertBefore

removeChild

21、面试官:说说你对BOM的理解,常见的BOM对象你了解哪些?

BOM(Browser Object Model),浏览器对象模型,提供了独立于内容与浏览器窗口进行交互的对象。其作用就是跟浏览器做一些交互效果,比如如何进行页面的后退,前进,刷新,浏览器的窗口发生变化,滚动条的滚动,以及获取客户的一些信息如:浏览器品牌版本,屏幕分辨率。

常见的BOM对象:

1、window

Bom的核心对象是window,它表示浏览器的一个实例

在浏览器中,window对象有双重角色,即是浏览器窗口的一个接口,又是全局对象

2、location

location属性描述如下

href

search

port

hostname

完整url

url的查询字符串,通常为?后面的内容

url的端口号,没有则为空

域名,不带端口号

3、navigator

navigator对象主要用来获取浏览器的属性,区分浏览器类型。属性较多,且兼容性比较复杂

如navigator.userAgent 返回浏览器的用户代理字符串

4、screen

保存的是浏览器窗口外面的客户端显示器的信息,比如像素宽度和像素高度

5、history

history对象主要用来操作浏览器URL的历史记录,可以通过参数向前,向后,或者向指定URL跳转,如history.go()

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

在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数,一般来说,递归需要有边界条件、递归前进阶段和递归返回阶段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。

尾递归,即在函数尾位置调用自身

在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储,递归次数过多容易造成栈溢出。这时候,我们就可以使用尾递归,即一个函数中所有递归形式的调用都出现在函数的末尾,对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误

应用场景:

数组求和以及数组扁平化等

23、面试官:Cookie、sessionStorage、localStorage的区别及应用场景

相同点:存储在客户端

不同点

1、cookie数据大小不能超过4k;sessionStorage和localStorage的存储比cookie大得多,可以达到5M+

2、cookie设置的过期时间之前一直有效;localStorage永久存储,浏览器关闭后数据不丢失除非主动删除数据;sessionStorage数据在当前浏览器窗口关闭后自动删除

3、cookie的数据会自动的传递到服务器;sessionStorage和localStorage数据保存在本地

应用场景:标记用户与跟踪用户行为的情况,推荐使用cookie。适合长期保存在本地的数据(令牌),推荐使用localStorage。敏感账号一次性登录,推荐使用sessionStorage

24、面试官:说说JS垃圾回收机制

项目中,如果存在大量不被释放的内存(堆/栈/上下文),页面性能会变得很慢。当某些代码操作不能被合理释放,就会造成内存泄漏。我们尽可能减少使用闭包,因为它会消耗内存。

浏览器垃圾回收机制/内存回收机制:

浏览器的Javascript具有自动垃圾回收机制,垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。

1、标记清除:在js中,最常用的垃圾回收机制是标记清除:当变量进入执行环境时,被标记为“进入环境”,当变量离开执行环境时,会被标记为“离开环境”。垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存

2、引用计数:当前内存被占用一次,计数累加1次,移除占用就减1,减到0时,浏览器就回收它。

优化手段:内存优化; 手动释放:取消内存的占用即可。

(1)堆内存:fn = null 【null:空指针对象】

(2)栈内存:把上下文中,被外部占用的堆的占用取消即可。

常见内存泄漏:

在 JS 中,常见的内存泄露主要有 4 种,全局变量、闭包、DOM 元素的引用、定时器

25、面试官:说说你对函数式编程的理解?优缺点?

相比命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而非设计一个复杂的执行过程。

优点:更简单的复用、更强大的组合性、减少代码量,提高维护性

缺点:性能较差、资源占用、递归陷阱

26、面试官:Javascript中如何实现函数缓存?函数缓存有哪些应用场景?

函数缓存,就是将函数运算过的结果进行缓存,本质上就是用空间(缓存存储)换时间(计算过程),常用于缓存数据计算结果和缓存对象

实现函数缓存主要依靠闭包、柯里化、高阶函数

应用场景:以下几种情况下,适合使用缓存:

1、对于昂贵的函数调用,执行复杂计算的函数

2、对于具有有限且高度重复输入范围的函数

3、对于具有重复输入值的递归函数

4、对于纯函数,即每次使用特定输入调用时返回相同输出的函数

27、面试官:说说Javascript 数字精度丢失的问题,如何解决?

0.1+ 0.2 === 0.3 // false

计算机存储双精度浮点数需要先把十进制数转换为二进制的科学记数法的形式,然后计算机以自己的规则存储二进制的科学记数法。因为存储时有位数限制(64位),并且某些十进制的浮点数在转换为二进制数时会出现无限循环,会造成二进制的舍入操作(0舍1入),当再转换为十进制时就造成了计算误差。

解决方案:可以处理一下得到我们期望的结果。建议使用 toPrecision 凑整并 parseFloat 转

成数字后再显示

28、面试官:什么是防抖和节流?有什么区别?如何实现?

本质上是优化高频率执行代码的一种手段

如:浏览器的 resize、scroll、keypress、mousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能

为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用防抖(debounce)和节流(throttle)的方式来减少调用频率

防抖: n 秒后执行该事件,若在n 秒内被重复触发,则重新计时

// fn是你要调用的函数,delay是防抖的时间
function debounce(fn, delay) {
  // timer是一个定时器
  let timer = null;
  // 返回一个闭包函数,用闭包保存timer确保其不会销毁,重复点击会清理上一次的定时器
  return function () {
    // 保存事件参数,防止fn函数需要事件参数里的数据
    let arg = arguments;
    // 调用一次就清除上一次的定时器
    clearTimeout(timer);
    // 开启这一次的定时器
    timer = setTimeout(() => {
      // 若不改变this指向,则会指向fn定义环境
      fn.apply(this, arg);
    }, delay)
  }
}

节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效

// 时间戳 & 定时器方法
function throttle(fn, delay) {
  // 初始化定时器
  let timer = null;
  // 上一次调用时间
  let prev = null;
  // 返回闭包函数
  return function () {
    // 现在触发事件时间
    let now = Date.now();
    // 触发间隔是否大于delay
    let remaining = delay - (now - prev);
    // 保存事件参数
    const args = arguments;
    // 清除定时器
    clearTimeout(timer);
    // 如果间隔时间满足delay
    if (remaining <= 0) {
      // 调用fn,并且将现在的时间设置为上一次执行时间
      fn.apply(this, args);
      prev = Date.now();
    } else {
      // 否则,过了剩余时间执行最后一次fn
      timer = setTimeout(() => {
        fn.apply(this, args)
      }, delay);
    }
  }
}

函数防抖关注一定时间连续触发的事件,只在最后执行一次。而函数节流一段时间内只执行一次,频率较高的事件中使用来提高性能

防抖应用场景

搜索框搜索输入,窗口大小resize等

节流应用场景

搜索框,搜索联想功能

29、面试官:如何判断一个元素是否在可视区域中?

可以通过offsetTop、scrollTop判断一个元素是否在可视区域

el.offsetTop- document.documentElement.scrollTop <= viewPortHeight

offsetTop,返回元素相对带有定位父元素上方的偏移

scrollTop,返回被卷去的上侧距离,返回数值不带单位。

根据上述公式计算元素是否在可视区域。

30、面试官:大文件上传如何做断点续传?

断点续传指的是在下载或上传时,将下载或上传任务人为的划分为几个部分

每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载。用户可以节省时间,提高速度

一般实现方式有两种:

1、服务器端返回,告知从哪开始

2、浏览器端自行处理

上传过程中将文件在服务器写为临时文件,等全部写完了(文件上传完),将此临时文件重命名为正式文件即可。

如果中途上传中断过,下次上传的时候根据当前临时文件大小,作为在客户端读取文件的偏移量,从此位置继续读取文件数据块,上传到服务器从此偏移量继续写入文件即可

实现思路:

整体思路比较简单,拿到文件,保存文件唯一性标识,切割文件,分段上传,每次上传一段,根据唯一性标识判断文件上传进度,直到文件的全部片段上传完毕

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

上拉加载及下拉刷新都依赖于用户交互

上拉加载:

判断页面触底我们需要先了解一下下面几个属性

scrollTop:滚动视窗的高度距离window顶部的距离,它会随着往上滚动而不断增加,初始值是0,它是一个变化的值

clientHeight:它是一个定值,表示屏幕可视区域的高度;

scrollHeight:页面不能滚动时也是存在的,此时scrollHeight等于clientHeight。scrollHeight表示body所有元素的总长度(包括body元素自身的padding)

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

scrollTop+ clientHeight >= scrollHeight

下拉刷新:

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

关于下拉刷新的原生实现,主要分成三步:

1、监听原生touchstart事件,记录其初始位置的值,e.touches[0].pageY;

2、监听原生touchmove事件,记录并计算当前滑动的位置值与初始位置值的差值,大于0表示向下拉动,并借助CSS3的translateY属性使元素跟随手势向下滑动对应的差值,同时也应设置一个允许滑动的最大值;

3、监听原生touchend事件,若此时元素滑动达到最大值,则触发callback,同时将translateY重设为0,元素回到初始位置

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

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

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

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

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

如何实现

1、同域名下的单点登录

cookie的domain属性设置为当前域的父域,并且父域的cookie会被子域所共享。path属性默认为web应用的上下文路径。利用 Cookie 的这个特点我们只需要将Cookie的domain属性设置为父域的域名(主域名),同时将 Cookie的path属性设置为根路径,将Session ID(或 Token)保存到父域中。这样所有的子域应用就都可以访问到这个Cookie。

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

2、不同域名下的单点登录

可以选择将 Session ID (或 Token )保存到浏览器的 LocalStorage 中,让前端在每次向后端发送请求时,主动将LocalStorage的数据传递给服务端

这些都是由前端来控制的,后端需要做的仅仅是在用户登录成功后,将 Session ID(或 Token)放在响应体中传递给前端

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

还有一种情况就是可以部署一个认证中心,用于专门处理登录请求的独立的 Web服务,但相对复杂。

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

常见的Web攻击方式有

1、XSS (Cross Site Scripting) 跨站脚本攻击

2、CSRF(Cross-siterequest forgery)跨站请求伪造

3、SQL注入攻击

一、XSS

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

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

XSS的预防

XSS攻击的两大要素:

1、攻击者提交而恶意代码 2、浏览器执行恶意代码

针对第一个要素,我们在用户输入的过程中,过滤掉用户输入的恶劣代码,然后提交给后端

防止浏览器执行恶意代码

如在使用 .innerHTML、document.write() 时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 .textContent

二、CSRF

CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求

利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作目的。

CSRF的预防

同源检测、双重Cookie验证等

三、SQL注入

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

预防方式

1、严格检查输入变量的类型和格式

2、过滤和转义特殊字符

3、对访问数据库的Web应用程序采用Web应用防火墙

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

我们也可将字符串常用的操作方法归纳为增、删、改、查

增:concat 用于将一个或多个字符串拼接成一个新字符串

删:举例letstringValue = "hello world"

slice()

substr()

substring()

stringValue.slice(3)

stringValue.substr(3)

stringValue.substring(3,7)

"lo world"

"lo world"

"lo w"

stringValue.slice(3, 7)

stringValue.substring(3,7)

"lo w"

"lo w"

改:常见的有:

1、trim()、trimLeft()、trimRight() //删除前、后或前后所有空格符,再返回新的字符串

2、repeat() //接收一个参数,表示要将字符串复制多少次

3、toLowerCase()、 toUpperCase() //大小写转化

查:

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

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

转换方法:

split:把字符串按照指定的分割符,拆分成数组中的每一项

letstr = "12+23+34"

letarr = str.split("+") // [12,23,34]

替换方法:replace()

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

lettext = "cat, bat, sat, fat";

letresult = text.replace("at", "ond");

console.log(result);// "cond, bat, sat, fat"

おすすめ

転載: blog.csdn.net/weixin_55608297/article/details/129371679