【面经系列】JS面经

【Q1】letconstvar 的区别

区别一:变量提升

var 是用于声明变量的关键字,它会带来 “变量提升” 的问题。

JS 引擎在解释执行一个作用域中的代码时,会优先将所有的 var 声明放在最前面处理,但只是声明,并不会进行赋值操作。

a = 2;
var a;//先处理这个,相当于把这行代码移到顶部
console.log(a);//2

而通过 letconst 声明的变量或常量不会出现这种现象。

区别二:全局变量

在全局作用域中,通过 var 关键字或者没有通过任何关键字声明或直接使用的变量,都将成为 window 的属性。

var a = 2;
console.log(window.a === a);//true

区别三:块级作用域

通过 letconst 定义的变量或常量会存在块级作用域的问题。常见的区别在于 for 循环的标记变量。如果用 var 定义,那么这个标记标量可以在循环外访问。

for(var i = 0;i < 10;i++){
     
    console.log(i)
}
console.log("这是循环外面的i:" + i)//10

上面的例子中,i 作为标记变量出了循环以外就毫无意义了,而且还污染外层的作用域。

然而,如果将 var 改为 let 就不会存在这样的问题了。

区别四:临时死区

在一个作用域中(无论是全局作用域,函数作用域甚至是块级作用域),凡是通过 letconst 关键字声明的变量,那么它就是这个作用域的唯一,不能在它们声明之前的任何地方使用。所以,在一个变量声明(通过 letconst 声明)之前的地方就称为这个变量的临时死区。

var a = 2;
function foo(){
     
    a = 3;
    let a = 2;
}
foo();//报错,a is not defined

如果在临时死区访问变量,则会报出异常。

区别五:常量

通过 var 关键字无法定义常量,ES6 引入的 const 可以定义一个常量。准确的说,const 定义的只是栈常量,而对于引用类型而言,堆空间中的内容是可变的。

【Q2】数据类型

一:有哪些数据类型?

JS 中共有7种数据类型,包括基本类型和引用类型。其中,基本数据类型有6种,分别是:

  • string
  • number
  • boolean
  • undefined
  • null
  • symbol

symbolES6 新增的数据类型,表示独一无二的值,通过 symbol() 函数生成(不能通过 new 调用)。

二:怎么区分数据类型?

通过 typeof 运算符,或者通过 instanceof 关键字来实现。但是这样真的可以吗??

答案显然是否定的,因为在 JS 中也是 “万物皆对象”,也就是说:

typeof [] //object
(() => {
     }) instanceof Object //ture
[] instanceof Object //true

所以,这两种方法都不是区分数据类型的最佳实现。具体的实现方法是通过 Object.prototype.toString() 来实现的:

Object.prototype.toString.call({
     })              // '[object Object]'
Object.prototype.toString.call([])              // '[object Array]'
Object.prototype.toString.call(() => {
     })        // '[object Function]'
Object.prototype.toString.call('seymoe')        // '[object String]'
Object.prototype.toString.call(1)               // '[object Number]'
Object.prototype.toString.call(true)            // '[object Boolean]'
Object.prototype.toString.call(Symbol())        // '[object Symbol]'
Object.prototype.toString.call(null)            // '[object Null]'
Object.prototype.toString.call(undefined)       // '[object Undefined]'

Object.prototype.toString.call(new Date())      // '[object Date]'
Object.prototype.toString.call(Math)            // '[object Math]'
Object.prototype.toString.call(new Set())       // '[object Set]'
Object.prototype.toString.call(new WeakSet())   // '[object WeakSet]'
Object.prototype.toString.call(new Map())       // '[object Map]'
Object.prototype.toString.call(new WeakMap())   // '[object WeakMap]'

你会发现,上面这种格式的结果似乎不是我们期望的,我们应该更期望它直接返回一个类型,而不是带有 object 这个字眼的字符串。所以,这里自己写了一个方法:

function checkType(obj) {
     
    return Object.prototype.toString.call(obj).slice(8,-1)
}

【Q3】谈谈你对原型和原型链的理解

每个 JS 函数都有一个属性 prototype ,该属性指向一个对象,这个对象就被称为原型对象。更准确的说,这个对象是这个函数的实例的原型对象。所有由这个函数构造出来的实例都有一个属性 __proto__ 指向原型对象。

在原型对象上,有一个属性 constructor 指向产生这些实例的构造函数。

可以将原型对象理解为实例的公共区域,定义在原型对象上的所有属性或方法都可以被实例共享。

那么,原型对象是怎么产生的呢?一般情况下,原型对象是由 Object 产生的。那么,Object 实例的原型的原型呢?如果每个原型对象还有原型,那么不就是无止尽了吗?答案显然是否定的,Object 实例的原型没有原型对象(null)了。

由此,多个原型组成的就是一条原型链,在 JS 中,访问一个对象中的属性或方法,首先在对象自身中查找,如果找到则返回,否则去这个对象的原型中查找,如果没找到,就去原型的原型中查找,一直找到 Object 的原型为止。如果最终没有找到,则返回 undefined

【面经系列】JS面经_第1张图片

【Q4】谈谈你对作用域和作用域链的理解

【简单理解】:
作用域是管理变量的一套规则,规定了如何查找变量。在 JS 中,有三种作用域:全局作用域,函数作用域和块级作用域。常见的作用域工作模型有两种:词法作用域和动态作用域。JS 采用的是词法作用域,词法作用域是静态的,作用域在代码编写阶段就已经确认了,不会在执行过程中改变。因为作用域是可以嵌套的,比如可以在函数中定义函数,那么就会产生嵌套作用域,多个嵌套作用域之间就形成了一条作用域链。当查找一个变量时,就会在当前作用域中查找,如果有则返回,否则会向上一级作用域中查找,一直到全局作用域,如果仍然没有找到,则会报错。

其实这样说不够具体,为了更好的说明这作用域和作用域链,先从代码执行顺序开始。

【深入理解】:
都知道 JS 代码不是按顺序一行一行执行的,比如说会有变量提升,函数提升。可以将这种现象理解为 “一段一段” 的执行,那 “段” 是如何划分的呢?其实就是 “可执行代码片段executable code)”。在 JS 中,有两种可执行代码片段,分别是全局代码和函数代码。当执行流进入一个可执行片段前,会做一些准备工作,这个工作就是创建执行上下文(也称为执行环境)。每个执行环境都有两个重要的属性:变量对象,作用域链

变量对象是与当前执行环境相关的数据作用域,用于存储当前执行环境中定义的变量和函数声明。在全局环境中,变量对象就是全局对象 window

在函数上下文中,用活动对象来表示变量对象。其实活动对象就是变量对象,只是变量对象是规范上的或者说是引擎实现上的,不可以在 JS 环境中访问,只有当进入一个执行环境中,这个环境的变量对象才会被激活,所以称为活动对象。

活动对象是在进入函数执行上下文的时候被创建的,它通过函数的 arguments 属性初始化。

执行上下文的代码会分为两个阶段处理:

  • 进入执行上下文
  • 代码执行

进入执行上下文,此时变量对象只包括三点:

  • 函数的所有参数,由形参和实参组成的对象,没有实参则为 undefined
  • 函数声明,由函数名称和值组成的一个对象,如果变量对象已经存在相同名称的属性,则完全替换这个属性,意味着函数声明的权重是最高的!
  • 变量声明,对使用 var 关键字声明的变量进行声明,但不赋值。

代码执行就是按照顺序一行一行的执行。

然而,在词法作用域的层级关系中,多个变量对象组成的就是一条作用域链。每个执行环境都有一条作用域链,当函数被创建时,就会将当前执行环境的所有父级变量对象保存到 [[Scopes]] 属性中(一个数组)。当查找一个变量时,会在当前执行环境中的变量对象中查找,如果找不到就往父级查找,一直找到全局对象,如果没有,则报错异常。

【Q5】函数中的this是如何确定的?

在回答这个问题之前,先要说明一个概念:类型。

Types are further subclassified into ECMAScript language types and specification types. —— 引自 ES 规范第八章

上面的意思是,在 ES 规范里面,类型有两种,分别是:语言类型和规范类型。

其中,语言类型就是我们非常熟悉的那7种,而规范类型似乎有点陌生。规范类型是用来描述 ES 语言结构和语言类型的,规范类型包括:Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, 和 Environment Record ,共7种。

简而言之,规范类型就是用来描述语言底层行为逻辑的。

要回答清楚这个问题就需要先来说明规范类型中的 Reference

规范文档中是这样来介绍的:

The Reference type is used to explain the behaviour of such operators as delete, typeof, and the assignment operators.

大致意思是,Reference 类型就是用来解释诸如 deletetypeof 以及赋值等操作行为的。

Reference 由三个部分组成:

  • base value
  • referenced name
  • strict reference

其中,base value 就是属性所在的对象或者 EnvironmentRecord ,它的值只可能是 undefined,一个 Object 对象,布尔值,字符串,数值或者 EnvironmentRecord 其中的一种。

referenced name 就是属性的名称。
strict reference 是一个布尔值,表示是否严格引用。

举例:

var foo = 1;

// 对应的Reference是:
var fooReference = {
    base: EnvironmentRecord,
    name: 'foo',
    strict: false
};
var foo = {
    bar: function () {
        return this;
    }
};

foo.bar(); // foo

// bar对应的Reference是:
var BarReference = {
    base: foo,
    propertyName: 'bar',
    strict: false
};

除了需要知道这些以外,还要再介绍三个方法:

  • GetBase(V) —— 返回 reference 中的 base value
  • IsPropertyReference() —— 表示是否为属性引用,意思是如果 base value 是一个对象,那么就返回 true
  • GetValue() —— 返回对象属性真正的值。

好了,可以来介绍如何决定 this 了:

  1. 计算 MemberExpression 的结果赋值给 ref
  2. 判断 ref 是不是一个 Reference 类型。
  • 如果是,并且 IsPropertyReference(ref)true ,那么 this 就是 GetBase(ref)
  • 如果是,并且 base valueEnvironment Record ,那么 this 就是 ImplicitThisValue(ref) 该方法的返回值永远是 undefined,也就是说 this 就是 undefined
  • 如果不是,那么 this 的值为 undefined

注:在非严格模式下,如果 thisundefined,那么会被隐式转换为全局对象。

上面又提到 MemberExpression ,是什么呢?

意为成员表达式,有以下五种:

  • PrimaryExpression —— 原始表达式
  • FunctionExpression —— 函数定义表达式
  • MemberExpression [ Expression ] —— 属性访问表达式
  • MemberExpression . IdentifierName —— 属性访问表达式
  • new MemberExpression Arguments —— 对象创建表达式
function foo() {
     
    console.log(this)
}

foo(); // MemberExpression 是 foo

function foo() {
     
    return function() {
     
        console.log(this)
    }
}

foo()(); // MemberExpression 是 foo()

var foo = {
     
    bar: function () {
     
        return this;
    }
}

foo.bar(); // MemberExpression 是 foo.bar

所以,可以简单的理解为 MemberExpression 就是 () 左边的内容。

确定了 MemberExpression 以后,接着就是判断 ref 是不是一个 Reference 类型。

判断规则:

  1. 只要使用了赋值运算符,逻辑运算符,逗号运算法等等一切触发了 GetValue 方法的,都不是 Reference,此时的 this 就是 undefined
  2. 解析标识符(通过函数名调用),通过对象调用这两种方式的 MemberExpression 都是 Reference 类型。

【总结】:
步骤一:计算 MemberExpression
步骤二:判断 MemberExpression 是否为 Reference 类型
步骤三:如果上一步结果为是,则判断 IsPropertyReference 是否为真,如果是,则 this 就是 GetBase(MemberExpression)
如果 base valueEnvironment Record ,那么 this 就是 undefined
步骤四:如果步骤二结果为否,那么 this 就是 undefined

最后,在非严格模式下,如果 thisundefined ,则会被隐式转换为全局对象。

【Q6】说说你对even loop 的理解(事件循环机制)

【基本概念】:
在浏览器端,JS 是单线程的,也就是说,在同一个时刻最多只有一个代码片段在执行,然而浏览器却可以很好得处理异步任务,这是因为除了 JS 主线程以外,在浏览器内部还有一个工作线程(也称为幕后线程)来协助处理异步任务。在主线程中有一个执行栈,所有的 JS 代码都会在执行栈中执行。在执行的过程中,如果遇到一些异步代码,比如 SetTimeoutajax 等等,那么浏览器就会将这些代码放到工作线程中执行,在前端由浏览器底层执行,这个线程的执行不会阻塞主线程的执行,主线程继续执行栈中的剩余代码。

当工作线程里面的代码执行完成后,该线程就会将它的回调函数放到任务队列(又称为事件队列,消息队列)中等待执行。当主线程执行完栈中的代码就会检查任务队列是否有任务要执行,如果有任务要执行,那么就将该任务放到执行栈中执行。如果当前的任务队列为空,它就会一直循环等待任务的到来,因此也称为事件循环。

【执行顺序】
从上面知道,工作线程会将异步的回调函数放到任务队列中,然后让主线程执行,如果队列中有多个任务,那么先执行哪个呢?

JS 中,有两个任务队列,一个是 macro task 宏任务(大任务),另一个是 micro task 微任务(小任务)。

常见的宏任务:

  • 主线程
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI render

常见的微任务:

  • Promise
  • async/await
  • process.nextTick
  • MutationObserver

事件循环机制是这样处理的:

检查宏任务队列是否为空,如果不为空,则执行宏任务队列中时间最长的(也就是最早进入宏任务队列的那个任务),执行完毕后,检查微任务队列,如果微任务队列中有微任务,那么执行所有微任务。当然,如果宏任务队列为空,那么一直等待微任务。

简而言之,一次实践循环只执行处于宏任务队列队首的任务,执行完成后,立即执行所有微任务。

举例:

console.log(1)
setTimeout(function() {
     
  //settimeout1
  console.log(2)
}, 0);
const intervalId = setInterval(function() {
     
  //setinterval1
  console.log(3)
}, 0)
setTimeout(function() {
     
  //settimeout2
  console.log(10)
  new Promise(function(resolve) {
     
    //promise1
    console.log(11)
    resolve()
  })
  .then(function() {
     
    console.log(12)
  })
  .then(function() {
     
    console.log(13)
    clearInterval(intervalId)
  })
}, 0);
//promise2
Promise.resolve()
  .then(function() {
     
    console.log(7)
  })
  .then(function() {
     
    console.log(8)
  })
console.log(9)

所以,输出顺序是:
1,9 —— 在主线程中,普通代码直接进入执行栈
7,8 —— 执行所有微任务
2 —— 执行宏任务,此时微任务队列为空
3 —— 同上
10 —— 里面包含的都是微任务,按顺序输出11,12,13

【Q7】谈谈你对闭包的理解

MDN 对闭包是这样定义的:

闭包是指那些能够访问自由变量的函数

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

举例:

var a = 1;

function foo() {
     
    console.log(a);
}

foo();

foo 函数可以访问变量 aa 不是参数也不是局部变量,所以此处 a 是自由变量。那么函数 foo 就是一个闭包。

从理论上讲,所有函数都是闭包!!!

ES 规范中,闭包是这样定义的:

  1. 从理论角度,所有的函数。因为他们都在创建的时候就将上下层上下文的数据保存起来了,哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于在访问自由变量了,所以所有的函数都是闭包。
  2. 从实践角度来讲,在代码中引用了自由变量,即使创建它的上下文已经销毁,但是它依然存在。

看一个例子:

var scope = "global scope";
function checkscope(){
     
    var scope = "local scope";
    function f(){
     
        return scope;
    }
    return f;
}

var foo = checkscope();
foo();

上面代码的执行过程:

  1. 执行流进入全局代码片段,创建全局执行上下文,全局执行上下文压入执行上下文栈
  2. 全局执行上下文初始化
  3. 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈
  4. checkscope 执行上下文初始化,创建变量对象、作用域链
  5. checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
  6. 执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈
  7. f 执行上下文初始化,创建变量对象、作用域链
  8. f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

f 函数执行的时候,checkscope 函数已经从执行上下文栈中弹出,怎么还会读取到 checkscope 作用域下的 scope 值呢?

原因就在于 f 函数的作用域链:

fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

就是因为这个作用域可以找到 checkscopeContext.AO (活动对象),即使 checkscopeContext 被销毁了,但是 JS 依然会让它的活动对象活在内存中,f 函数依然可以通过作用域链访问它。

【总结】:
闭包就是能访问自由变量的函数,通常表现为嵌套函数,外层函数返回内层函数,内层函数引用了外层函数的局部变量,从而导致外层函数销毁时,通过内层函数还能访问到其变量对象。

【Q8】怎么实现单例?

单例,就是一个类只能有一个实例对象。

let Person = (function() {
     
    let instance
    function Person() {
     }
    return function () {
     
        return instance || (instance = new Person())
    }
})()

let obj = new Person()
let obj1 = new Person()
obj.name = "jonas"
obj1.name = "jerry"
console.log(obj) //jerry
console.log(obj1) //jerry  ===> 两次实例化产生的是同一个对象,达到了单例的效果

【总结】:

匿名函数自调用,返回一个实例。

【Q9】JSON.stringfy()JSON.parse() 有什么作用?有多少个参数?

【首先回答 JSON.stringfy()

JSON.stringfy() 可以将一个 JS 对象或数组转换为一个 JSON 字符串。方法接受三个参数,第一个参数表示要序列化为 JSON 字符串的值;第二个参数比较复杂,有三种情况:

  • 如果参数是一个函数,则在序列化过程中,被序列化的值都会经过该函数的转换和处理。
  • 如果参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中。
  • 如果参数是 null 或未提供,则对象对象中的所有属性都会被序列化。

简而言之,第二个参数是用于转换,处理,过滤属性的。

第三个参数用于指定缩进用的空白字符串,用于美化输出。

【接着回答 JSON.parse()

JSON.parse() 用于解析 JSON 字符串,是 JSON.stringfy() 的逆操作。返回一个 JS 对象或数组。

该方法接收两个参数,第一个是需要被解析的 JSON 字符串;第二个参数可以传入一个函数,用于修改解析生成的原始值,调用的时机在返回结果前。

【最后,回答他们配合起来使用的作用】

上面的两个方法配合起来可以实现简单的深拷贝 (引出深拷贝)。此时,面试官很可能会沿着深拷贝继续问下去,所以,下一个问题就是怎么实现深拷贝了。

【Q10】如何实现深拷贝?

也许你会觉得奇怪,上面不是说了通过两个方法的配合就可以实现深拷贝了吗?

是的,不过只回答这种简单的方法面试官可能会不太满意。因为通过 JSON 序列化的方式来实现是有缺陷的,毕竟 JSON 只支持一部分的 JS 数据类型进行序列化,如果涉及到一些数据 JSON 是不支持的或者序列化数据中存在循环引用,那么就无法实现深拷贝了。

【举例:一个对象有一个属性指向自己本身

let obj = {
     name: "joans"}
obj.obj = obj

JSON.parse(JSON.stringify(obj)) //TypeError: Converting circular structure to JSON

【基础版】

function clone(obj) {
     
    if(typeof obj === "object"){
     
        let target = Array.isArray(obj) ? [] : {
     }
        for(let key in obj){
     
            target[key] = clone(obj[key]) //关键在于递归
        }
        return target
    }else{
     
        return obj
    }
}

这种方法并没有处理循环引用的问题,如果数据存在循环引用,那么会导致内存溢出。

【进阶版:解决循环引用】

解决循环引用的问题,只需要额外开辟一个存储空间即可。用一个存储空间来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,如果有则直接返回,否则继续拷贝。

function clone(obj,map = new Map()) {
     
    if(typeof obj === "object"){
     
        let target = Array.isArray(obj) ? [] : {
     }
        if(map.get(obj)){
     
            return map.get(obj)
        }
        map.set(obj,target)
        for(let key in obj){
     
            target[key] = clone(obj[key],map)
        }
        return target
    }else{
     
        return obj
    }
}

【暂告一段落】
当然,到此为止还没达到一份满意的答案,比如说还没处理其他类型的数据,由于涉及代码量大篇幅长问题,此处先不展开,日后另写一篇关于深拷贝的博文。

【Q11】你知道websocket吗?使用过哪些API?

【websocket是什么】

websocket 是一种网络通信协议,是一种相比 HTTP 协议更为 “平等” 的协议。HTTP 协议只能由客户端发起通信,服务器不能主动跟客户端通信。然而,websocket 协议(简称 ws 协议)两端都能主动发起通信。

【相关API】

【构造函数】

WebSocket 是一个构造函数,通过它可以进行实例化。函数接受一个服务器 url 为参数,表示与该服务器建立连接。

【属性】

  • readyState —— 返回实例对象当前的状态,共有四种:

    CONNECTING:值为0,表示正在连接。
    OPEN:值为1,表示连接成功,可以通信了。
    CLOSING:值为2,表示连接正在关闭。
    CLOSED:值为3,表示连接已经关闭,或者打开连接失败。

  • bufferedAmount —— 表示当前还有多少字节的二进制数据没有发送出去,用于判断发送是否结束。

【方法】

  • ws.onopen() —— 参数为连接成功的回调函数
  • ws.onmessage() —— 客户端返回信息时回调
  • ws.onerror() —— 发生错误时的回调
  • ws.onclose() —— 连接关闭时回调
  • ws.send() —— 发送数据

【Q12】怎么实现跨域?

【回答什么是跨域】

跨域是指浏览器不能执行其他网站的脚本。 是由浏览器的 同源策略 造成的,是浏览器对 JS 实施的安全限制。

所谓的同源是指:协议名,域名,端口三者都相同。

同源策略限制了以下行为:

  • CookieLocalStorage 无法读取
  • DOMJS 对象无法获取
  • ajax 请求发送不出去

【解决跨域的方法一:jsonp】

【原理】:浏览器允许 script 标签来引入外部资源(包括其他域下的资源),所以可以通过 script 标签来完成跨域。

【具体实现】:动态创建 script 标签,设置 src 属性来引入外部资源。

【代码实现】:

let script = document.createElement('script');

script.src = '协议名://域名:端口号?callback=callback';
//插入到body中
document.body.appendChild(script);
//响应回调函数中获取响应内容
function callback(res) {
     
  console.log(res);
}

通过这种方式实现跨域非常简单,但是有一个缺点:这种方式只能实现 get 请求。

【解决跨域的方法二:CORS】

【简单回答什么是CORS】:

CORS 是一个 W3C 标准,全称是跨域资源共享,它允许浏览器向跨源服务器发出请求。它需要浏览器和服务器同时支持,只要服务器实现了 CORS 接口,就可以跨源通信。

【两种处理方式】:

CORS 根据不同的请求有不同的处理方式。

【第一种:简单请求】

只要满足下面条件的就是简单请求:

  • 请求方式为 HEADPOST 或者 GET
  • http 头信息不超出以下字段:Accept、Accept-Language 、 Content-Language、 Last-Event-ID、 Content-Type(限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain)

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

Origin 字段用于说明来自哪个源

【第二种:非简单请求】

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 PUTDELETE 或者,Content-Typeapplication/json

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。

【Q13】事件冒泡与事件捕获有什么区别?

【基本概念】

事件冒泡和事件捕获分别由微软和网景公司提出的,这两个概念都是为了解决页面中事件流的问题而产生的。

比如说,页面中存在以下结构:

<div id="outer">
    <p id="inner">Click me!p>
div>

假设两个元素都绑定了一个点击事件的处理函数,那么哪个函数会先被触发呢?

为了解决这个问题,就有了事件冒泡和事件捕获这两个概念了。

【事件冒泡】

微软认为事件流是从内往外的,也就是说从最内层的发生元素开始,一直向外传播,直到 document 对象。

比如,在上面的例子中,事件的流向为: p -> div -> body -> html -> document

【事件捕获】

事件捕获与事件冒泡是相反的,它会从最外层流向最内层。

比如,上面的例子中,事件的流向为:document -> html -> body -> div -> p

【W3C标准】

因为当时的两家公司主张完全相反的事件流,所以 W3C 来制定了一个标准:事件先捕获再冒泡。

在现代浏览器中,事件的回调函数默认都是在事件冒泡阶段触发的,如果需要在事件捕获阶段触发回调函数,则需要用到一个方法:addEventListener()

该方法有三个参数:

  • 第一个参数是事件名
  • 第二个参数是事件的回调函数
  • 第三个参数决定在哪个阶段触发回调函数,默认值为 false ,表示在事件冒泡阶段触发回调函数。

【Q14】cookie与session有什么区别?

【cookie】

cookie 是服务器在本地机器上存储的小段文本,并随每个请求发送同一个服务器。通俗的讲,就是一个服务器让本地存储了一个文本,然后发往这个服务器的所有请求都必须带上这个文本。

这种机制采用的是在客户端保持状态的方案。

cookie 的主要内容由5个部分组成:

  • 名字
  • 过期时间
  • 路径

其中,路径和域一起构成一个 cookie 的作用范围。若不设置过期时间,则表示这个 cookie 的生命周期为浏览器会话期间,一旦浏览器关闭窗口,则失活,这种 cookie 也被称为会话 cookie 。这种 cookie 一般存储在内存中,而不存储在磁盘中。若设置了过期时间,浏览器就会把 cookie 保存到硬盘上。

【session】

session 机制采用的是一种在服务器端保持状态的解决方案。

当程序需要为某个客户端的请求创建一个 session 时,服务器首先检查这个客户端的请求里是否已包含了一个 session 标识(称为 session id),如果已包含则说明以前已经为此客户端创建过session,服务器就按照 session id 把这个 session 检索出来使用(检索不到,会新建一个),如果客户端请求不包含 session id,则为此客户端创建一个 session 并且生成一个与此 session相关联的 session idsession id 的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个 session id 将被在本次响应中返回给客户端保存。

保存 session id 的方式可以采用 cookie 。但是 cookie 机制用户可以禁止,所以经常被使用的是 url 重写,在 url 后面带上 session id

【区别一:存储能力不同】

cookie 只能保存 ASCII 字符串,不能存储 Unicode 字符串或二进制数据。

session 中能够存储任何类型的数据,包括字符串,数值类型,集合类型,甚至是 Java Bean 等。

【区别二:隐私策略不同】

cookie 存储在客户端中,对客户端是可见的,其他程序有可能会窥探、复制甚至修改 cookie 中的内容。

然而,session 存储在服务器端就省事多了,只要保护好服务器则不会被修改和窥探。

【区别三:对服务器压力不同】

cookie 是存储在客户端中的,而 session 存储在服务器端,从而就会给服务器带来压力,耗费内存。cookie 则不占用服务器资源。

【区别四:跨域支持不同】

cookie 支持跨域名访问,session 不支持跨域名访问。

【Q15】谈谈防抖节流原理,区别以及应用

参考我的一篇文章:https://blog.csdn.net/weixin_41030302/article/details/104636034

(未完,待更)

你可能感兴趣的:(面经系列,js)