面试官 | JavaScript系列

40 阅读17分钟

各循环区别

  • for of: 用于数组,也可以用于字符串
  • for in:用于对象,也可以用于数组,不过他会遍历原型链上的属性,所以不变使用
  • 都可以使用break中断
  • 注意!!,二者迭代时都不会改变数组值,他是原数组当中的副本,不是原数组的值

数组和字符串的区别

  • 数组:之后可以改变其长度内容等
  • 字符串:创建之后是无法根据索引来改变其长度或内容,但是提供了很多方法来操作字符串,比如concat连接两个字符串(str1.concat(str3,str2···)),replace可以替换字符串内容,split,slice等等一些方法(str.replace(old,new),返回新的字符串)

Number和parsetInt的区别

  • Number:接受一个参数,将整个字符串解析成数值类型的值,如果存在不是数字的值,返回nan -parsetInt:接受两个参数,一个是转换字符串,一个为这个字符串的进制,而且她转换成的是一个整数,比如42.5,转换会变成42,并且他也可以处理正负号

hasOwnProperty

该方法是检测对象本身是否拥有某个属性,而不包括其原型对象上的属性

isPrototypeOf&instanceof

  • instanceof :dog instanceof animal.用于检查一个对象是否是另一个对象的原型
  • isPrototypeof : animal.protototype.isPrototypeOf(dog).用于检查一个对象是否是某个构造函数的实例

constructor&&proto&&prototype

帮你彻底搞懂JS中的prototype、__proto__与constructor(图解) - 掘金 (juejin.cn)

  • proto:对象所有。指向对象的原型对象
  • constructor: 对象所有
  • prototype: 函数所有。从函数指向对象,指向函数的原型对象

数据类型

promise

深拷贝浅拷贝

浅拷贝

  • Object.assgin:
  • slice() -...扩展运算符

深拷贝

  • _.cloneDeep(被复制对象)
  • jQuery.extend()
const $ = require('jquery');
const obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
const obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false
  • JSON.stringify():JSON.parse(JSON.stringify(obj1));会忽略undefinedsymbol函数 手写深拷贝
function deepClone(obj, hash = new WeakMap()) {
  // 如果是 null 或者不是对象类型,则直接返回原始值
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 如果对象已经被拷贝过,直接返回拷贝后的对象,避免无限递归
  if (hash.has(obj)) {
    return hash.get(obj);
  }

  // 根据原对象的构造函数创建一个新的对象
  //确保深拷贝后的对象具有与原对象相同的类型和原型链结构
  const cloneObj = new obj.constructor();

  // 记录已拷贝过的对象,避免无限递归
  hash.set(obj, cloneObj);

  // 遍历原对象的属性,递归调用 deepClone 进行深拷贝
  for (let key in obj) {
    // 用于检查对象自身是否包含指定的属性(不包括原型链上的属性)
    // 为什么这里需要判断
    // 因为它会遍历对象自身的属性以及它的原型链上的属性。当我们使用 obj.hasOwnProperty(key) 进行判断时,是为了确保只复制对象自身的属性,而不复制原型链上的属性。
    if (obj.hasOwnProperty(key)) {
      console.log("1111",key, obj[key]);

      cloneObj[key] = deepClone(obj[key], hash);
      console.log("clone",cloneObj[key])
    }
  }

  // 返回深拷贝后的对象
  return cloneObj;
}

const originalObj = {
  name: 'John',
  age: 30,
  hobbies: ['reading', 'cooking'],
  address: {
    city: 'New York',
    zip: '10001'
  }
};

const clonedObj = deepClone(originalObj);
console.log("clonedObj",clonedObj)

this对象

image.png

执行上下文和执行栈

执行上下文:是一种对JavaScript代码执行环境的一种描述,也就是说只要有js的代码运行,那么它就一定在执行上下文当中。一般分为三种,全局执行上下文,函数执行上下文,eval函数执行上下文。 执行上下文的生命周期:创建-执行-回收,创建做了:确定this的值、词法环境创建、变量环境创建,变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性,在es6当中,两者的区别在于,词法环境用于存储函数声明和变量let、const的绑定,变量环境用于存储变量var的绑定。

执行栈:后进先出结构,用来存储在代码执行的期间创建的所有执行上下文

事件模型

JS中的事件冒泡、事件捕获、事件委托 - 掘金 (juejin.cn)

事件流:流描述了页面接收事件的顺序。为什么会出现这么一个概念呢,就好比很多个同心圆,我们点一下圆心,会发现我们点击的是所有圆圈,那么类似的原理,DOM是一个树状的结构,我们点击一个按钮,不光是点击了这个按钮,还包括他的外层容器。我记得我有意思的是,IE和另外一家公司提出了完全不一样的解决办法,IE支持事件冒泡流,另外一家公司netscape支持事件捕获流。在事件冒泡流,最先触发的是从当前元素,然后不断沿着DOM树向上依次触发节点直到document对象(现代会一直到window)。事件捕获钟,事件首先由document捕获再不断沿着DOM树向下传播,直到到达目标元素。(通常使用事件冒泡)。在DOM2Events当中规定了事件流分为三个阶段,事件捕获、到达目标和事件冒泡,事件捕获最先发生。

阻止事件冒泡:添加e.stopPropagation()

事件委托/事件代理:就是将处理事件绑定到祖先元素上,不管是哪个子孙元素触发,都能沿着DOM树向上冒泡,到达祖先元素并且触发事件。就比如我们有100个button需要注册,可以循环给每个按钮添加(可以但没必要),这样就增加了内存的消耗,我们可以给他们的父元素绑定这个事件。注意只有冒泡事件才可以用

没有冒泡的事件

  • focus:获得焦点
  • blur:失去焦点

instanceof && typeof

typeof:返回类型(存在bug,null会返回object),可以用来判断基本数据类型,而引用数据类型除了函数其他都返回object。有什么作用呢,我们可以用typeof来代替判断某个变量是否存在,因为如果我们用if的话,如果这个变量没有声明就会报错。

instanceof:他的原理是检测构造函数的prototype属性是否出现在实例对象的原型链上,返回的布尔值。所以他判断的是实例对象,比如字符串和字符串对象是不一样的,如果我们判断一个简单的字符串,instanceof会返回false,而通过new String构造的实例对象才会是True。注意:这里还要注意一个问题

如果需要通用判断的话可以用object.prototype.toString.call(),返回格式:"[object 类型]"

new

new会执行以下操作

  • 在内存中创建一个新的对象
  • 新对象的Prototype被赋予构造函数的prototype属性
  • 构造函数内部的this要指向这个新对象
  • 执行构造函数的代码
  • 如果构造函数返回非空对象,则返回该对象,否则返回新创建的对象 image.png

手写new

使用apply的方式实现:

  • ...xx:接收任意参数并打包成数组
  • 为什么会返回undefined:因为当apply是改变this执行fFunc
function nnew(fFunc,...xx){
  // 构造一个新对象
  let obj={}

  // 赋予prototype属性
  obj.__proto__ =fFunc.prototype

  // 将构造函数的this指向新对象
  let res=fFunc.apply(fFunc,xx)
  console.log("res",res)

  // 非空对象就返回该对象,否则返回新对象
  //这里我们当然希望返回新对象,因为新对象才继承
  return res instanceof Object ? res : obj
}

返回res的情况

function iinit(name,age){
  this.name=name
  this.age=age
}
iinit.prototype.say=function(){
  console.log('name',this.name)
  console.log('age',this.age)
}
let p=nnew(iinit,'x,12',12)
console.log(p)

返回obj的情况

打印为函数返回结果,并不能继承,因为他没有经过new的创建过程,也就是说该对象没有经过原型链的继承

function iinit(name,age){
  this.name=name
  this.age=age
  return {}
}
iinit.prototype.say=function(){
  console.log('name',this.name)
  console.log('age',this.age)
}
let p=nnew(iinit,'x,12',12)
console.log(p)

使用call方法实现:

//其余相同,这里只给出改动的代码
function nnew(fFunc,xx){
........
let res=fFunc.call(fFunc,...xx)
......
let p=nnew(iinit,['x,12',12])
.....

使用bind实现

function nnew(fFunc,xx){
  let obj={}
  obj.__proto__ =fFunc.prototype
  let res=fFunc.bind(obj)
  //重点在这里,必须要手动执行
  let rres=res(xx)
  return rres instanceof Object ? rres : obj
}

call、apply、bind

三者都是改变this指向,不同的是,call和apply都是立即执行,而bind是返回一个绑定this的函数,call和apply的区别是,call接收的参数列表,而apply接收的是一个数组

bind实现

Function.prototype.myBind = function (context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
      throw new TypeError("Error");
  }

  // 获取参数
  const args = [...arguments].slice(1),
        fn = this;

  return function Fn() {

      // 根据调用方式,传入不同绑定值
      console.log("newnewnew",new fn(...arguments),this instanceof Fn)
      // 如果this是函数,就new一个函数,this指向这个新函数,如果不是函数
      return fn.apply(this instanceof Fn ? new fn(...arguments) : context, args.concat(...arguments)); 
  }
}



function greet(message) {
  console.log(`${message} ${this.name}!`);
}

const person = {
  name: "Alice"
};

const boundGreet = greet.myBind(person, "Hello");
boundGreet(); // 输出:Hello Alice!

事件循环

js是一门单线程语言,那我们就会想到阻塞的问题,而单线程实现非阻塞的方法就是事件循环。在js执行的时候,会把任务分为同步任务和异步任务,同步任务会放入执行栈当中立即执行,而异步任务会放入宿主环境当中进行等待,等到结束之后放入任务队列,当执行栈当中的代码执行完成之后,就会到任务队列当中查看是否有异步任务,如果有就取出来放到执行栈当中立即执行,从执行栈到任务队列不断查看的这个过程就叫做事件循环

异步任务当中又分为宏任务和微任务

  • 微任务:Promise.then\process.nextTick(node.js)
  • 宏任务:script\setTimeout\I/O(node)

async用来声明一个异步方法,而await等待异步方法执行,await会阻塞后面的代码,阻塞的代码放入微任务队列

DOM(文档对象模型)常见操作

核心对象为document

  • 创建节点:createElement
  • 查询节点:querySelectorquerSelectorAll、``
document.getElementsByClassName('class属性值');返回拥有指定class的对象集合
document.getElementsByTagName('标签名');返回拥有指定标签名的对象集合
document.getElementsByName('name属性值'); 返回拥有指定名称的对象结合
document/element.querySelectorAll('CSS选择器');   返回所有匹配的元素

document/element.querySelector('CSS选择器');  仅返回第一个匹配的元素
document.getElementById('id属性值');返回拥有指定id的对象的引用
document.documentElement;  获取页面中的HTML标签
document.body; 获取页面中的BODY标签
document.all[''];  获取页面中的所有元素节点的对象集合型
  • 更新节点:
  • innerHTML:可以解析HTML文档;获取元素可以获取到html结构
  • innerText:不可以解析html文档;获取元素不可以获取html结构,且能识别节点样式。
  • textContext:不可以解析html文档;获取元素不可以获取html结构,不能识别节点样式

三者打印区别:

image.png

  • 添加节点:
  • appendChild
  • insertBeforebox.insertBefore(新节点,插入位置节点)
  • setAttribute: div.setAttribute('属性名称','属性值')
  • 删除节点
  • removeChild(删除节点):不在文档树,但是还在内存当中,使用时,必须获取节点本身和他的父节点
const self = document.getElementById('to-be-removed');
const parent = self.parentElement;
parent.removeChild(self);

BOM(浏览器对象模型)

核心对象为window

  • window
  • location
  • navigate
  • screen
  • history:history.go()

内存泄漏

没有释放不能再使用的内存

垃圾回收

  • 标记清除:变量进入执行环境就被标记为‘进入环境’,离开环境就被标记“离开环境”
var m = 0,n = 19 // 把 m,n标记为进入环境。
add(m, n) // 把 a, b, c标记为进入环境。
console.log(n) // a,b,c标记为离开环境,等待垃圾回收。
function add(a, b) {
  a++
  var c = a + b
  return c
}

引用计数: 给每一个对象分配一个引用计数器,对象被创建的初始值为1,被引用一次那么值就+1,反之-1

存储

http缓存

强缓存

就是浏览器访问某个某个资源的时候会判断是否使用本地缓存里已经存在的资源,如果使用的话,就不会发送请求到服务器,从而达到减轻服务器访问压力的作用,在强缓存当中是否使用缓存是有浏览器来确定的

强缓存的优先级大于协商缓存

使用:

在响应头当中设置Cache-Control

  • max-age=31536000,max-age代表缓存时间,单位为秒,意味着在一年内浏览器不会再向服务器发送请求
  • no-cache: 不使用强缓存(但仍会使用协商缓存)。
  • no-store: 不使用缓存(不使用强缓存也不使用协商缓存),每次都向服务器发送资源请求。
  • private: 只允许客户端使用缓存,不允许其他代理服务器进行缓存。
  • public: 客户端和代理服务器都可缓存。
  • s-maxage: 与max-age类似,区别是s-maxage是设定代理服务器的缓存时间。

缓存资源位置

磁盘缓存(disk cache)和内存缓存(memory cache)

  • 较大资源--磁盘缓存,较小资源--内存缓存
  • 内存缓存速度>磁盘缓存速度

Expires也可以实现强缓存,但是Expires版本低于Cache-Control

协商缓存

协商缓存是服务器告诉浏览器是否使用缓存,也就是说每一次的请求都会发送到服务器

使用:

浏览器初次请求资源,服务器返回资源的同时会生成一个Etag携带在响应头当中返回给浏览器,当浏览器再次请求资源的时候会在请求头里携带If-None-Match,他的值是之前服务器返回的Etag的值,服务器拿到If-None-Match的值会和现在服务器生成的Etag值进行比对,如果:

  • 没有变化返回304,就告诉浏览器使用缓存,不会返回资源文件
  • 发生变化返回200,返回最新资源给浏览器使用

浏览器缓存

cookie

  • 通常随浏览器关闭失效,数据很小4kb左右。cookie分为持久性:设置了过期时间,无论你是否关闭浏览器,他都会在时间到了自动删除, 会话级别 就是没有设置过期时间,或者过期时间短,那么就会随浏览器关闭而失效。
  • 不安全容易被篡改,(明文、过期时间设置过长、未使用https进行安全传输)

解决不安全:

  • cookie设置为HttpOnly,减少xss(跨站脚本攻击)攻击风险document.cookie = "myCookie=value; expires=Thu, 01 Jan 2023 00:00:00 UTC; path=/; HttpOnly";
  • 使用https
  • 可以进行签名
//设置
document.cookie = 'username=John;

//获取
document.cookie

//删除,要重新设置cookie的过期时间为一个过去的时间,使其失效
document.cookie = 'username=; 
expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'

localStorage

  • 除非手动删除,否则理论上来说是永久有效的,5mb左右
  • 可以主动携带到后端
//存储
localStorage.setItem('username', 'John')

//获取
localStorage.getItem(key)

//删除
localStorage.removeItem('username')

// 设置Axios的全局默认请求头,将LocalStorage的数据添加到请求头中 axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorageData;

sessionStorage

  • 随网页关闭而被清除
  • 使用方法和localStorage相同

函数缓存

函数柯里化:接收多个参数的函数转换成接受单一参数的函数

// 函数柯里化
var add2 = function (x) {
    //**返回函数**
    return function (y) {
        return x+y;
    }
}
add2(3)(4) //7

闭包:内部函数可以访问外部函数的变量

应用场景

function init(){
  let sum=0
 return function add(b){
    sum=sum+b;
    return sum;
  }
}

const add1=init()
console.log(add1(1))
console.log(add1(2))
console.log(add1(3))

高阶函数:接收函数作为参数或者返回函数

  • map函数,接收一个函数和数组作为参数
  • filter:接收函数作为参数,函数定义过滤条件

精度丢失问题

0.1+0.2===0.3 是错的,因为计算机存储浮点数,会先转成二进制的科学计数法的形式,但是某些十进制数的浮点数转换成二进制会出现无限循环,就会被舍入,再转换成十进制就会造成误差。这个例子,当0.1和0.2转换成二进制舍入之后,计算得出的十进制刚好是一个接近于0.3的浮点数,于是就出现了不相等的情况

解决办法:

  • 使用toPrecision(指定字符串尾数),再通过parseFloat转换成数字(parsetFloat的作用就是将字符串转换成浮点数)
  • 封装的库Math.js

防抖节流

防抖:规定时间后再执行事件,若重复触发,就重新计数

<script>
    function send(){
        console.log('我们好棒鸭!');
    }
    const btn = document.getElementById("btn");
    btn.addEventListener("click",debounce(send,1000))
    
    function debounce(send,time){
        let name;
         let if_first=true
        return function(){
        let arr=arguments
            if(if_first) send(this,...arr)
             if_first=false
            if(name) clearTimeout(name) //name有值,说明上次定时器还没执行完,这时清除上次定时器
            name = setTimeout(function(){
            send(this,...arr)
        }, time);
    }
}
</script>

节流:事件触发之后,下一次触发要间隔规定的时间

<script>
    function send(){
        console.log('我们好棒鸭!',this);
    }
    const btn = document.getElementById("btn");
    btn.addEventListener("click",throttle(send,1000))

    function throttle(send,delay){
        let pretime = Date.now()
        let if_first=true
        return function(){
        
        let arr = arguments //包含传递给函数的每个参数
        if(if_first) send(this,...arr)
        if_first=false
        if(Date.now() - pretime > delay){//
        send.call(this,...arr)//此时this就是指向btn
        pretime = Date.now()
        }
    }
}
</script>

大文件断点续传

  • 分片上传:将所要上传的文件切分成多个数据块,再在服务器上对文件进行汇总
  • 断点续传:将上传下载任务人为的分为几个部分,每一个部分使用一个线程,如果遇到网络故障可以从已经上传下载的位置继续开始而不用从头,提高速度节省时间。

web常见攻击

  • Dos攻击:向服务器发送大量无效请求,耗尽资源
  • DDos攻击:利用多个主机同时向目标服务器发起攻击
  • DRDos攻击: 一种特殊的DDos工具,利用有漏洞的服务器作为反射器,向目标服务器发送大量伪造请求,是服务器浏览增加导致服务不可用
  • XSS:cross site scripting,跨站脚本攻击
  • CSRF:cross-site request forgery,跨站请求伪造。攻击者诱导受害者进入第三方网站,利用受害者在被攻击网站已经获取的注册凭证,在第三方网站中,向被攻击网站发送跨站请求
  • 解决方法:使用CSRF令牌()、使用同源策略(保证网页只能请求同源的资源)
  • SQl注入

SSO的单点登录

单点登录就是,有很多个互相信任系统,当我们在其中一个系统登陆时,再跳转到其它系统就会自动帮用户登录,就比如我们平时登录天猫,再打开淘宝就会帮我们自动登录,这就是单点登录。

同域名单点登录: cookie的domain属性设置为当前父域,父域的cookie就会被子域所共享。(tieba.baidu.com和,map.baidu.com都在baidu.com这个主域名,就可以使用这种方法)

不同域名单点登录

  • 方法一
  • 用户在统一认证中心验证,token写入中心的cookie
  • 登陆后跳转目标应用,将token拼接在url后面
  • 应用系统拿到token之后,向认证中心确认合法性
  • 正确就将该token写入应用系统的cookie当中
  • 方法二:利用iframe+postMessage
  • 在网页当中嵌套一个不可见的iframe
  • 利用postMessage将token传递给网页

<body>

<!-- 嵌套的 iframe,每个 iframe 有不同的域 -->
<iframe id="iframe1" src="http://localhost:5501/test.html" width="400" height="300"></iframe>
<script>
  const iframes = document.querySelectorAll('iframe');
  const token = 'your_shared_token';

  // 向每个 iframe 发送共享的 Token
  function sendTokenToIframes() {
    iframes.forEach(iframe => {
      iframe.contentWindow.postMessage({token}, iframe.src);
    });
  }

  // 在加载完成后发送 Token
  iframes.forEach(iframe => {
    iframe.onload = sendTokenToIframes;
  });
</script>

</body>

子:

<script>
  // 监听来自父窗口的消息
  window.addEventListener('message', function(event) {
    // 验证消息来源是否是预期的
    if (event.origin === 'http://127.0.0.1:5501') {
      // 处理收到的消息
      const receivedToken = event.data;
      console.log('Received token:', receivedToken);
    }
  });
</script>