关于javascript:最新前端面试总结

2次阅读

共计 39773 个字符,预计需要花费 100 分钟才能阅读完成。

介绍 plugin

插件零碎是 Webpack 胜利的一个关键性因素。在编译的整个生命周期中,Webpack 会触发许多事件钩子,Plugin 能够监听这些事件,依据需要在相应的工夫点对打包内容进行定向的批改。

一个最简略的 plugin 是这样的:

class Plugin{
      // 注册插件时,会调用 apply 办法
      // apply 办法接管 compiler 对象
      // 通过 compiler 上提供的 Api,能够对事件进行监听,执行相应的操作
      apply(compiler){
          // compilation 是监听每次编译循环
          // 每次文件变动,都会生成新的 compilation 对象并触发该事件
        compiler.plugin('compilation',function(compilation) {})
      }
}

注册插件:

// webpack.config.js
module.export = {
    plugins:[new Plugin(options),
    ]
}

事件流机制:

Webpack 就像工厂中的一条产品流水线。原材料通过 Loader 与 Plugin 的一道道解决,最初输入后果。

  • 通过链式调用,按程序串起一个个 Loader;
  • 通过事件流机制,让 Plugin 能够插入到整个生产过程中的每个步骤中;

Webpack 事件流编程范式的外围是根底类 Tapable,是一种 观察者模式 的实现事件的订阅与播送:

const {SyncHook} = require("tapable")

const hook = new SyncHook(['arg'])

// 订阅
hook.tap('event', (arg) => {
    // 'event-hook'
    console.log(arg)
})

// 播送
hook.call('event-hook')

Webpack 中两个最重要的类 CompilerCompilation 便是继承于 Tapable,也领有这样的事件流机制。

  • Compiler : 能够简略的了解为 Webpack 实例,它蕴含了以后 Webpack 中的所有配置信息,如 options,loaders, plugins 等信息,全局惟一,只在启动时实现初始化创立,随着生命周期逐个传递;
  • Compilation: 能够称为 编译实例。当监听到文件产生扭转时,Webpack 会创立一个新的 Compilation 对象,开始一次新的编译。它蕴含了以后的输出资源,输入资源,变动的文件等,同时通过它提供的 api,能够监听每次编译过程中触发的事件钩子;
  • 区别:

    • Compiler 全局惟一,且从启动生存到完结;
    • Compilation对应每次编译,每轮编译循环均会从新创立;
  • 罕用 Plugin:

    • UglifyJsPlugin: 压缩、混同代码;
    • CommonsChunkPlugin: 代码宰割;
    • ProvidePlugin: 主动加载模块;
    • html-webpack-plugin: 加载 html 文件,并引入 css / js 文件;
    • extract-text-webpack-plugin / mini-css-extract-plugin: 抽离款式,生成 css 文件;DefinePlugin: 定义全局变量;
    • optimize-css-assets-webpack-plugin: CSS 代码去重;
    • webpack-bundle-analyzer: 代码剖析;
    • compression-webpack-plugin: 应用 gzip 压缩 js 和 css;
    • happypack: 应用多过程,减速代码构建;
    • EnvironmentPlugin: 定义环境变量;
  • 调用插件 apply 函数传入 compiler 对象
  • 通过 compiler 对象监听事件

loader 和 plugin 有什么区别?

webapck 默认只能打包 JS 和 JOSN 模块,要打包其它模块,须要借助 loader,loader 就能够让模块中的内容转化成 webpack 或其它 laoder 能够辨认的内容。

  • loader就是模块转换化,或叫加载器。不同的文件,须要不同的 loader 来解决。
  • plugin是插件,能够参加到整个 webpack 打包的流程中,不同的插件,在适合的机会,能够做不同的事件。

webpack 中都有哪些插件,这些插件有什么作用?

  • html-webpack-plugin 主动创立一个 HTML 文件,并把打包好的 JS 插入到 HTML 文件中
  • clean-webpack-plugin 在每一次打包之前,删除整个输入文件夹下所有的内容
  • mini-css-extrcat-plugin 抽离 CSS 代码,放到一个独自的文件中
  • optimize-css-assets-plugin 压缩 css

为什么应用 Virtual DOM

  • 手动操作 DOM 比拟麻烦,还须要思考浏览器兼容性问题,尽管有 jQuery 等库简化 DOM 操作,然而随着我的项目的简单 DOM 操作简单晋升
  • 为了简化 DOM 的简单操作于是呈现了各种 MVVM 框架,MVVM 框架解决了视图和状态的同步问题
  • 为了简化视图的操作咱们能够应用模板引擎,然而模板引擎没有解决跟踪状态变动的问题,于是Virtual DOM 呈现了
  • Virtual DOM 的益处是当状态扭转时不须要立刻更新 DOM,只须要创立一个虚构树来形容 DOMVirtual DOM 外部将弄清楚如何无效(diff) 的更新 DOM
  • 虚构 DOM 能够维护程序的状态,跟踪上一次的状态
  • 通过比拟前后两次状态的差别更新实在 DOM

虚构 DOM 的作用

  • 保护视图和状态的关系
  • 简单视图状况下晋升渲染性能
  • 除了渲染 DOM 以外,还能够实现 SSR(Nuxt.js/Next.js)、原生利用 (Weex/React Native)、小程序(mpvue/uni-app) 等

Generator

GeneratorES6中新增的语法,和 Promise 一样,都能够用来异步编程。Generator 函数能够说是 Iterator 接口的具体实现形式。Generator 最大的特点就是能够管制函数的执行。

  • function* 用来申明一个函数是生成器函数,它比一般的函数申明多了一个 *,* 的地位比拟随便能够挨着 function 关键字,也能够挨着函数名
  • yield 产出的意思,这个关键字只能呈现在生成器函数体内,然而生成器中也能够没有yield 关键字,函数遇到 yield 的时候会暂停,并把 yield 前面的表达式后果抛出去
  • next作用是将代码的控制权交还给生成器函数
function *foo(x) {let y = 2 * (yield (x + 1))
  let z = yield (y / 3)
  return (x + y + z)
}
let it = foo(5)
console.log(it.next())   // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8, done: false}
console.log(it.next(13)) // => {value: 42, done: true}

下面这个示例就是一个 Generator 函数,咱们来剖析其执行过程:

  • 首先 Generator 函数调用时它会返回一个迭代器
  • 当执行第一次 next 时,传参会被疏忽,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
  • 当执行第二次 next 时,传入的参数等于上一个 yield 的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 12,所以第二个 yield 等于 2 12 / 3 = 8
  • 当执行第三次 next 时,传入的参数会传递给 z,所以 z = 13, x = 5, y = 24,相加等于 42

yield理论就是暂缓执行的标示,每执行一次 next(),相当于指针挪动到下一个yield 地位

总结一下 Generator 函数是 ES6 提供的一种异步编程解决方案。通过 yield 标识位和 next() 办法调用,实现函数的分段执行

遍历器对象生成函数,最大的特点是能够交出函数的执行权

  • function 关键字与函数名之间有一个星号;
  • 函数体外部应用 yield表达式,定义不同的外部状态;
  • next指针移向下一个状态

这里你能够说说 Generator的异步编程,以及它的语法糖 asyncawiat,传统的异步编程。ES6 之前,异步编程大抵如下

  • 回调函数
  • 事件监听
  • 公布 / 订阅

传统异步编程计划之一:协程,多个线程相互合作,实现异步工作。

// 应用 * 示意这是一个 Generator 函数
// 外部能够通过 yield 暂停代码
// 通过调用 next 复原执行
function* test() {
  let a = 1 + 2;
  yield 2;
  yield 3;
}
let b = test();
console.log(b.next()); // >  {value: 2, done: false}
console.log(b.next()); // >  {value: 3, done: false}
console.log(b.next()); // >  {value: undefined, done: true}

从以上代码能够发现,加上 *的函数执行后领有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数能够继续执行被暂停的代码。以下是 Generator 函数的简略实现

// cb 也就是编译过的 test 函数
function generator(cb) {return (function() {
    var object = {
      next: 0,
      stop: function() {}
    };

    return {next: function() {var ret = cb(object);
        if (ret === undefined) return {value: undefined, done: true};
        return {
          value: ret,
          done: false
        };
      }
    };
  })();}
// 如果你应用 babel 编译后能够发现 test 函数变成了这样
function test() {
  var a;
  return generator(function(_context) {while (1) {switch ((_context.prev = _context.next)) {
        // 能够发现通过 yield 将代码宰割成几块
        // 每次执行 next 函数就执行一块代码
        // 并且表明下次须要执行哪块代码
        case 0:
          a = 1 + 2;
          _context.next = 4;
          return 2;
        case 4:
          _context.next = 6;
          return 3;
        // 执行结束
        case 6:
        case "end":
          return _context.stop();}
    }
  });
}

Vue 响应式原理

Vue 的响应式原理是外围是通过 ES5 的爱护对象的 Object.defindeProperty 中的拜访器属性中的 get 和 set 办法,data 中申明的属性都被增加了拜访器属性,当读取 data 中的数据时主动调用 get 办法,当批改 data 中的数据时,主动调用 set 办法,检测到数据的变动,会告诉观察者 Wacher,观察者 Wacher 主动触发从新 render 以后组件(子组件不会从新渲染), 生成新的虚构 DOM 树,Vue 框架会遍历并比照新虚构 DOM 树和旧虚构 DOM 树中每个节点的差异,并记录下来,最初,加载操作,将所有记录的不同点,部分批改到实在 DOM 树上。

  • 虚构 DOM (Virtaul DOM): 用 js 对象模仿的,保留以后视图内所有 DOM 节点对象根本形容属性和节点间关系的树结构。用 js 对象,形容每个节点,及其父子关系,造成虚构 DOM 对象树结构。
  • 因为只有在 data 中申明的根本数据类型的数据,根本不存在数据不响应问题,所以重点介绍数组和对象在 vue 中的数据响应问题,vue 能够检测对象属性的批改,但无奈监听数组的所有变动及对象的新增和删除,只能应用数组变异办法及 $set 办法。

能够看到,arrayMethods 首先继承了 Array,而后对数组中所有能扭转数组本身的办法,如 pushpop 等这些办法进行重写。重写后的办法会先执行它们自身原有的逻辑,并对能减少数组长度的 3 个办法 pushunshiftsplice 办法做了判断,获取到插入的值,而后把新增加的值变成一个响应式对象,并且再调用 ob.dep.notify() 手动触发依赖告诉,这就很好地解释了用 vm.items.splice(newLength) 办法能够检测到变动

总结:Vue 采纳数据劫持联合公布—订阅模式的办法,通过 Object.defineProperty() 来劫持各个属性的 setter,getter,在数据变动时公布音讯给订阅者,触发相应的监听回调。

  • Observer 遍历数据对象,给所有属性加上 settergetter,监听数据的变动
  • compile 解析模板指令,将模板中的变量替换成数据,而后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,增加监听数据的订阅者,一旦数据有变动,收到告诉,更新视图

Watcher 订阅者是 ObserverCompile 之间通信的桥梁,次要做的事件

  • 在本身实例化时往属性订阅器 (dep) 外面增加本人
  • 待属性变动 dep.notice() 告诉时,调用本身的 update() 办法,并触发 Compile 中绑定的回调

Object.defineProperty(),那么它的用法是什么,以及优缺点是什么呢?

  • 能够检测对象中数据产生的批改
  • 对于简单的对象,层级很深的话,是不敌对的,须要经行深度监听,这样子就须要递归到底,这也是它的毛病。
  • 对于一个对象中,如果你新减少属性,删除属性,Object.defineProperty()是不能观测到的,那么应该如何解决呢?能够通过 Vue.set()Vue.delete()来实现。
// 模仿 Vue 中的 data 选项 
let data = {msg: 'hello'}
// 模仿 Vue 的实例 
let vm = {}
// 数据劫持: 当拜访或者设置 vm 中的成员的时候,做一些干涉操作
Object.defineProperty(vm, 'msg', {// 可枚举(可遍历)
  enumerable: true,
  // 可配置(能够应用 delete 删除,能够通过 defineProperty 从新定义) 
  configurable: true,
  // 当获取值的时候执行 
  get () {console.log('get:', data.msg)
    return data.msg 
  },
  // 当设置值的时候执行 
  set (newValue) {console.log('set:', newValue) 
    if (newValue === data.msg) {return}
    data.msg = newValue
    // 数据更改,更新 DOM 的值 
    document.querySelector('#app').textContent = data.msg
  } 
})

// 测试
vm.msg = 'Hello World' 
console.log(vm.msg)

Vue3.x 响应式数据原理

Vue3.x改用 Proxy 代替 Object.defineProperty。因为Proxy 能够间接监听 对象和数组 的变动,并且有多达 13 种拦挡办法。并且作为新规范将受到浏览器厂商重点继续的性能优化。

Proxy只会代理对象的第一层,那么 Vue3 又是怎么解决这个问题的呢?

判断以后 Reflect.get 的 返回值是否为 Object,如果是则再通过reactive 办法做代理,这样就实现了深度观测。

监测数组的时候可能触发屡次 get/set,那么如何避免触发屡次呢?

咱们能够判断 key 是否为以后被代理对象 target 本身属性,也能够判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger

// 模仿 Vue 中的 data 选项 
let data = {
  msg: 'hello',
  count: 0 
}
// 模仿 Vue 实例
let vm = new Proxy(data, {
  // 当拜访 vm 的成员会执行
  get (target, key) {console.log('get, key:', key, target[key])
    return target[key]
  },
  // 当设置 vm 的成员会执行
  set (target, key, newValue) {console.log('set, key:', key, newValue)
    if (target[key] === newValue) {return}
    target[key] = newValue
    document.querySelector('#app').textContent = target[key]
  }
})

// 测试
vm.msg = 'Hello World'
console.log(vm.msg)

Proxy 相比于 defineProperty 的劣势

  • 数组变动也能监听到
  • 不须要深度遍历监听

ProxyES6 中新增的性能,能够用来自定义对象中的操作

let p = new Proxy(target, handler);
// `target` 代表须要增加代理的对象
// `handler` 用来自定义对象中的操作
// 能够很不便的应用 Proxy 来实现一个数据绑定和监听

let onWatch = (obj, setBind, getLogger) => {
  let handler = {get(target, property, receiver) {getLogger(target, property)
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {setBind(value);
      return Reflect.set(target, property, value);
    }
  };
  return new Proxy(obj, handler);
};

let obj = {a: 1}
let value
let p = onWatch(obj, (v) => {value = v}, (target, property) => {console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2

总结

  • Vue

    • 记录传入的选项,设置 $data/$el
    • data 的成员注入到 Vue 实例
    • 负责调用 Observer 实现数据响应式解决(数据劫持)
    • 负责调用 Compiler 编译指令 / 插值表达式等
  • Observer

    • 数据劫持

      • 负责把 data 中的成员转换成 getter/setter
      • 负责把多层属性转换成 getter/setter
      • 如果给属性赋值为新对象,把新对象的成员设置为 getter/setter
    • 增加 DepWatcher 的依赖关系
    • 数据变动发送告诉
  • Compiler

    • 负责编译模板,解析指令 / 插值表达式
    • 负责页面的首次渲染过程
    • 当数据变动后从新渲染
  • Dep

    • 收集依赖,增加订阅者(watcher)
    • 告诉所有订阅者
  • Watcher

    • 本身实例化的时候往 dep 对象中增加本人
    • 当数据变动 dep 告诉所有的 Watcher 实例更新视图

类型及检测形式

1. JS 内置类型

JavaScript 的数据类型有下图所示

其中,前 7 种类型为根底类型,最初 1 种(Object)为援用类型,也是你须要重点关注的,因为它在日常工作中是应用得最频繁,也是须要关注最多技术细节的数据类型

  • JavaScript一共有 8 种数据类型,其中有 7 种根本数据类型:UndefinedNullBooleanNumberStringSymboles6新增,示意举世无双的值)和 BigIntes10 新增);
  • 1 种援用数据类型——Object(Object 实质上是由一组无序的名值对组成的)。外面蕴含 function、Array、Date等。JavaScript 不反对任何创立自定义类型的机制,而所有值最终都将是上述 8 种数据类型之一。

    • 援用数据类型: 对象Object(蕴含一般对象 -Object,数组对象 -Array,正则对象 -RegExp,日期对象 -Date,数学函数 -Math,函数对象 -Function

在这里,我想先请你重点理解上面两点,因为各种 JavaScript 的数据类型最初都会在初始化之后放在不同的内存中,因而下面的数据类型大抵能够分成两类来进行存储:

  • 原始数据类型:根底类型存储在栈内存,被援用或拷贝时,会创立一个齐全相等的变量;占据空间小、大小固定,属于被频繁应用数据,所以放入栈中存储。
  • 援用数据类型:援用类型存储在堆内存,存储的是地址,多个援用指向同一个地址,这里会波及一个“共享”的概念;占据空间大、大小不固定。援用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找援用值时,会首先检索其在栈中的地址,获得地址后从堆中取得实体。

JavaScript 中的数据是如何存储在内存中的?

在 JavaScript 中,原始类型的赋值会残缺复制变量值,而援用类型的赋值是复制援用地址。

在 JavaScript 的执行过程中,次要有三种类型内存空间,别离是 代码空间 栈空间 堆空间 。其中的代码空间次要是存储可执行代码的,原始类型(Number、String、Null、Undefined、Boolean、Symbol、BigInt) 的数据值都是间接保留在“栈”中的,援用类型 (Object) 的值是寄存在“堆”中的。因而在栈空间中(执行上下文),原始类型存储的是变量的值,而援用类型存储的是其在 ” 堆空间 ” 中的地址,当 JavaScript 须要拜访该数据的时候,是通过栈中的援用地址来拜访的,相当于多了一道转手流程。

在编译过程中,如果 JavaScript 引擎判断到一个闭包,也会在堆空间创立换一个 “closure(fn)” 的对象(这是一个外部对象,JavaScript 是无法访问的),用来保留闭包中的变量。所以闭包中的变量是存储在“堆空间”中的。

JavaScript 引擎须要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都寄存在栈空间外面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。通常状况下,栈空间都不会设置太大,次要用来寄存一些原始类型的小数据。而援用类型的数据占用的空间都比拟大,所以这一类数据会被寄存到堆中,堆空间很大,能寄存很多大的数据,不过毛病是分配内存和回收内存都会占用肯定的工夫。因而须要“栈”和“堆”两种空间。

题目一:老成持重

let a = {
  name: 'lee',
  age: 18
}
let b = a;
console.log(a.name);  // 第一个 console
b.name = 'son';
console.log(a.name);  // 第二个 console
console.log(b.name);  // 第三个 console

这道题比较简单,咱们能够看到第一个 console 打进去 name 是 ‘lee’,这应该没什么疑难;然而在执行了 b.name=’son’ 之后,后果你会发现 a 和 b 的属性 name 都是 ‘son’,第二个和第三个打印后果是一样的,这里就体现了援用类型的“共享”的个性,即这两个值都存在同一块内存中共享,一个产生了扭转,另外一个也随之跟着变动。

你能够间接在 Chrome 控制台敲一遍,深刻了解一下这部分概念。上面咱们再看一段代码,它是比题目一稍简单一些的对象属性变动问题。

题目二:渐入佳境

let a = {
  name: 'Julia',
  age: 20
}
function change(o) {
  o.age = 24;
  o = {
    name: 'Kath',
    age: 30
  }
  return o;
}
let b = change(a);     // 留神这里没有 new,前面 new 相干会有专门文章解说
console.log(b.age);    // 第一个 console
console.log(a.age);    // 第二个 console

这道题波及了 function,你通过上述代码能够看到第一个 console 的后果是 30b 最初打印后果是 {name: "Kath", age: 30};第二个 console 的返回后果是 24,而 a 最初的打印后果是 {name: "Julia", age: 24}

是不是和你料想的有些区别?你要留神的是,这里的 functionreturn 带来了不一样的货色。

起因在于:函数传参进来的 o,传递的是对象在堆中的内存地址值,通过调用 o.age = 24(第 7 行代码)的确扭转了 a 对象的 age 属性;然而第 12 行代码的 return 却又把 o 变成了另一个内存地址,将 {name: "Kath", age: 30} 存入其中,最初返回 b 的值就变成了 {name: "Kath", age: 30}。而如果把第 12 行去掉,那么 b 就会返回 undefined

2. 数据类型检测

(1)typeof

typeof 对于原始类型来说,除了 null 都能够显示正确的类型

console.log(typeof 2);               // number
console.log(typeof true);            // boolean
console.log(typeof 'str');           // string
console.log(typeof []);              // object     []数组的数据类型在 typeof 中被解释为 object
console.log(typeof function(){});    // function
console.log(typeof {});              // object
console.log(typeof undefined);       // undefined
console.log(typeof null);            // object     null 的数据类型被 typeof 解释为 object

typeof 对于对象来说,除了函数都会显示 object,所以说 typeof 并不能精确判断变量到底是什么类型, 所以想判断一个对象的正确类型,这时候能够思考应用 instanceof

(2)instanceof

instanceof 能够正确的判断对象的类型,因为外部机制是通过判断对象的原型链中是不是能找到类型的 prototype

console.log(2 instanceof Number);                    // false
console.log(true instanceof Boolean);                // false 
console.log('str' instanceof String);                // false  
console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);                   // true    
// console.log(undefined instanceof Undefined);
// console.log(null instanceof Null);
  • instanceof 能够精确地判断简单援用数据类型,然而不能正确判断根底数据类型;
  • typeof 也存在弊病,它尽管能够判断根底数据类型(null 除外),然而援用数据类型中,除了 function 类型以外,其余的也无奈判断
// 咱们也能够试着实现一下 instanceof
function _instanceof(left, right) {
    // 因为 instance 要检测的是某对象,须要有一个前置判断条件
    // 根本数据类型间接返回 false
    if(typeof left !== 'object' || left === null) return false;

    // 取得类型的原型
    let prototype = right.prototype
    // 取得对象的原型
    left = left.__proto__
    // 判断对象的类型是否等于类型的原型
    while (true) {if (left === null)
            return false
        if (prototype === left)
            return true
        left = left.__proto__
    }
}

console.log('test', _instanceof(null, Array)) // false
console.log('test', _instanceof([], Array)) // true
console.log('test', _instanceof('', Array)) // false
console.log('test', _instanceof({}, Object)) // true

(3)constructor

console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true

这里有一个坑,如果我创立一个对象,更改它的原型,constructor就会变得不牢靠了

function Fn(){};

Fn.prototype=new Array();

var f=new Fn();

console.log(f.constructor===Fn);    // false
console.log(f.constructor===Array); // true 

(4)Object.prototype.toString.call()

toString()Object 的原型办法,调用该办法,能够对立返回格局为 “[object Xxx]” 的字符串,其中 Xxx 就是对象的类型。对于 Object 对象,间接调用 toString() 就能返回 [object Object];而对于其余对象,则须要通过 call 来调用,能力返回正确的类型信息。咱们来看一下代码。

Object.prototype.toString({})       // "[object Object]"
Object.prototype.toString.call({})  // 同上后果,加上 call 也 ok
Object.prototype.toString.call(1)    // "[object Number]"
Object.prototype.toString.call('1')  // "[object String]"
Object.prototype.toString.call(true)  // "[object Boolean]"
Object.prototype.toString.call(function(){})  // "[object Function]"
Object.prototype.toString.call(null)   //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(/123/g)    //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call([])       //"[object Array]"
Object.prototype.toString.call(document)  //"[object HTMLDocument]"
Object.prototype.toString.call(window)   //"[object Window]"

// 从下面这段代码能够看出,Object.prototype.toString.call() 能够很好地判断援用类型,甚至能够把 document 和 window 都辨别开来。

实现一个全局通用的数据类型判断办法,来加深你的了解,代码如下

function getType(obj){
  let type  = typeof obj;
  if (type !== "object") {    // 先进行 typeof 判断,如果是根底数据类型,间接返回
    return type;
  }
  // 对于 typeof 返回后果是 object 的,再进行如下的判断,正则返回后果
  return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1');  // 留神正则两头有个空格
}
/* 代码验证,须要留神大小写,哪些是 typeof 判断,哪些是 toString 判断?思考下 */
getType([])     // "Array" typeof []是 object,因而 toString 返回
getType('123')  // "string" typeof 间接返回
getType(window) // "Window" toString 返回
getType(null)   // "Null" 首字母大写,typeof null 是 object,需 toString 来判断
getType(undefined)   // "undefined" typeof 间接返回
getType()            // "undefined" typeof 间接返回
getType(function(){}) // "function" typeof 能判断,因而首字母小写
getType(/123/g)      //"RegExp" toString 返回

小结

  • typeof

    • 间接在计算机底层基于数据类型的值(二进制)进行检测
    • typeof nullobject 起因是对象存在在计算机中,都是以000 开始的二进制存储,所以检测进去的后果是对象
    • typeof 一般对象 / 数组对象 / 正则对象 / 日期对象 都是object
    • typeof NaN === 'number'
  • instanceof

    • 检测以后实例是否属于这个类的
    • 底层机制:只有以后类呈现在实例的原型上,后果都是 true
    • 不能检测根本数据类型
  • constructor

    • 反对根本类型
    • constructor 能够轻易改,也不准
  • Object.prototype.toString.call([val])

    • 返回以后实例所属类信息

判断 Target 的类型,单单用 typeof 并无奈齐全满足,这其实并不是 bug,实质起因是 JS 的万物皆对象的实践。因而要真正完满判断时,咱们须要辨别看待:

  • 根本类型(null): 应用 String(null)
  • 根本类型 (string / number / boolean / undefined) + function: – 间接应用 typeof 即可
  • 其余援用类型 (Array / Date / RegExp Error): 调用toString 后依据 [object XXX] 进行判断

3. 数据类型转换

咱们先看一段代码,理解下大抵的状况。

'123' == 123   // false or true?
''== null    // false or true?'' == 0        // false or true?
[] == 0        // false or true?
[] == ''       // false or true?
[] == ![]      // false or true?
null == undefined //  false or true?
Number(null)     // 返回什么?Number('')      // 返回什么?parseInt('');    // 返回什么?{}+10           // 返回什么?let obj = {[Symbol.toPrimitive]() {return 200;},
    valueOf() {return 300;},
    toString() {return 'Hello';}
}
console.log(obj + 200); // 这里打印进去是多少?

首先咱们要晓得,在 JS 中类型转换只有三种状况,别离是:

  • 转换为布尔值
  • 转换为数字
  • 转换为字符串

转 Boolean

在条件判断时,除了 undefinednullfalseNaN''0-0,其余所有值都转为 true,包含所有对象

Boolean(0)          //false
Boolean(null)       //false
Boolean(undefined)  //false
Boolean(NaN)        //false
Boolean(1)          //true
Boolean(13)         //true
Boolean('12')       //true

对象转原始类型

对象在转换类型的时候,会调用内置的 [[ToPrimitive]] 函数,对于该函数来说,算法逻辑一般来说如下

  • 如果曾经是原始类型了,那就不须要转换了
  • 调用 x.valueOf(),如果转换为根底类型,就返回转换的值
  • 调用 x.toString(),如果转换为根底类型,就返回转换的值
  • 如果都没有返回原始类型,就会报错

当然你也能够重写 Symbol.toPrimitive,该办法在转原始类型时调用优先级最高。

let a = {valueOf() {return 0},
  toString() {return '1'},
  [Symbol.toPrimitive]() {return 2}
}
1 + a // => 3

四则运算符

它有以下几个特点:

  • 运算中其中一方为字符串,那么就会把另一方也转换为字符串
  • 如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"
  • 对于第一行代码来说,触发特点一,所以将数字 1 转换为字符串,失去后果 '11'
  • 对于第二行代码来说,触发特点二,所以将 true 转为数字 1
  • 对于第三行代码来说,触发特点二,所以将数组通过 toString转为字符串 1,2,3,失去后果 41,2,3

另外对于加法还须要留神这个表达式 'a' + + 'b'

'a' + + 'b' // -> "aNaN"
  • 因为 + 'b' 等于 NaN,所以后果为 "aNaN",你可能也会在一些代码中看到过 + '1'的模式来疾速获取 number 类型。
  • 那么对于除了加法的运算符来说,只有其中一方是数字,那么另一方就会被转为数字
4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN

比拟运算符

  • 如果是对象,就通过 toPrimitive 转换对象
  • 如果是字符串,就通过 unicode 字符索引来比拟
let a = {valueOf() {return 0},
  toString() {return '1'}
}
a > -1 // true

在以上代码中,因为 a 是对象,所以会通过 valueOf 转换为原始类型再比拟值。

强制类型转换

强制类型转换形式包含 Number()parseInt()parseFloat()toString()String()Boolean(),这几种办法都比拟相似

  • Number() 办法的强制转换规则
  • 如果是布尔值,truefalse 别离被转换为 10
  • 如果是数字,返回本身;
  • 如果是 null,返回 0
  • 如果是 undefined,返回 NaN
  • 如果是字符串,遵循以下规定:如果字符串中只蕴含数字(或者是 0X / 0x 结尾的十六进制数字字符串,容许蕴含正负号),则将其转换为十进制;如果字符串中蕴含无效的浮点格局,将其转换为浮点数值;如果是空字符串,将其转换为 0;如果不是以上格局的字符串,均返回 NaN;
  • 如果是 Symbol,抛出谬误;
  • 如果是对象,并且部署了 [Symbol.toPrimitive],那么调用此办法,否则调用对象的 valueOf() 办法,而后根据后面的规定转换返回的值;如果转换的后果是 NaN,则调用对象的 toString() 办法,再次按照后面的程序转换返回对应的值。
Number(true);        // 1
Number(false);       // 0
Number('0111');      //111
Number(null);        //0
Number('');          //0
Number('1a');        //NaN
Number(-0X11);       //-17
Number('0X11')       //17

Object 的转换规则

对象转换的规定,会先调用内置的 [ToPrimitive] 函数,其规定逻辑如下:

  • 如果部署了 Symbol.toPrimitive 办法,优先调用再返回;
  • 调用 valueOf(),如果转换为根底类型,则返回;
  • 调用 toString(),如果转换为根底类型,则返回;
  • 如果都没有返回根底类型,会报错。
var obj = {
  value: 1,
  valueOf() {return 2;},
  toString() {return '3'},
  [Symbol.toPrimitive]() {return 4}
}
console.log(obj + 1); // 输入 5
// 因为有 Symbol.toPrimitive,就优先执行这个;如果 Symbol.toPrimitive 这段代码删掉,则执行 valueOf 打印后果为 3;如果 valueOf 也去掉,则调用 toString 返回 '31'(字符串拼接)
// 再看两个非凡的 case:10 + {}
// "10[object Object]",留神:{}会默认调用 valueOf 是{},不是根底类型持续转换,调用 toString,返回后果 "[object Object]",于是和 10 进行 '+' 运算,依照字符串拼接规定来,参考 '+' 的规定 C
[1,2,undefined,4,5] + 10
// "1,2,,4,510",留神 [1,2,undefined,4,5] 会默认先调用 valueOf 后果还是这个数组,不是根底数据类型持续转换,也还是调用 toString,返回 "1,2,,4,5",而后再和 10 进行运算,还是依照字符串拼接规定,参考 '+' 的第 3 条规定

‘==’ 的隐式类型转换规定

  • 如果类型雷同,毋庸进行类型转换;
  • 如果其中一个操作值是 null 或者 undefined,那么另一个操作符必须为 null 或者 undefined,才会返回 true,否则都返回 false
  • 如果其中一个是 Symbol 类型,那么返回 false
  • 两个操作值如果为 string 和 number 类型,那么就会将字符串转换为 number
  • 如果一个操作值是 boolean,那么转换成 number
  • 如果一个操作值为 object 且另一方为 stringnumber 或者 symbol,就会把 object 转为原始类型再进行判断(调用 objectvalueOf/toString 办法进行转换)。
null == undefined       // true  规定 2
null == 0               // false 规定 2
''== null              // false 规定 2'' == 0                 // true  规定 4 字符串转隐式转换成 Number 之后再比照
'123' == 123            // true  规定 4 字符串转隐式转换成 Number 之后再比照
0 == false              // true  e 规定 布尔型隐式转换成 Number 之后再比照
1 == true               // true  e 规定 布尔型隐式转换成 Number 之后再比照
var a = {
  value: 0,
  valueOf: function() {
    this.value++;
    return this.value;
  }
};
// 留神这里 a 又能够等于 1、2、3
console.log(a == 1 && a == 2 && a ==3);  //true f 规定 Object 隐式转换
// 注:然而执行过 3 遍之后,再从新执行 a == 3 或之前的数字就是 false,因为 value 曾经加上去了,这里须要留神一下

‘+’ 的隐式类型转换规定

‘+’ 号操作符,不仅能够用作数字相加,还能够用作字符串拼接。仅当 ‘+’ 号两边都是数字时,进行的是加法运算;如果两边都是字符串,则间接拼接,毋庸进行隐式类型转换。

  • 如果其中有一个是字符串,另外一个是 undefinednull 或布尔型,则调用 toString() 办法进行字符串拼接;如果是纯对象、数组、正则等,则默认调用对象的转换方法会存在优先级,而后再进行拼接。
  • 如果其中有一个是数字,另外一个是 undefinednull、布尔型或数字,则会将其转换成数字进行加法运算,对象的状况还是参考上一条规定。
  • 如果其中一个是字符串、一个是数字,则依照字符串规定进行拼接
1 + 2        // 3  惯例状况
'1' + '2'    // '12' 惯例状况
// 上面看一下非凡状况
'1' + undefined   // "1undefined" 规定 1,undefined 转换字符串
'1' + null        // "1null" 规定 1,null 转换字符串
'1' + true        // "1true" 规定 1,true 转换字符串
'1' + 1n          // '11' 比拟非凡字符串和 BigInt 相加,BigInt 转换为字符串
1 + undefined     // NaN  规定 2,undefined 转换数字相加 NaN
1 + null          // 1    规定 2,null 转换为 0
1 + true          // 2    规定 2,true 转换为 1,二者相加为 2
1 + 1n            // 谬误  不能把 BigInt 和 Number 类型间接混合相加
'1' + 3           // '13' 规定 3,字符串拼接

整体来看,如果数据中有字符串,JavaScript 类型转换还是更偏向于转换成字符串,因为第三条规定中能够看到,在字符串和数字相加的过程中最初返回的还是字符串,这里须要关注一下

null 和 undefined 的区别?

  • 首先 UndefinedNull 都是根本数据类型,这两个根本数据类型别离都只有一个值,就是 undefinednull
  • undefined 代表的含意是未定义,null 代表的含意是空对象(其实不是真的对象,请看上面的留神!)。个别变量申明了但还没有定义的时候会返回 undefinednull 次要用于赋值给一些可能会返回对象的变量,作为初始化。

其实 null 不是对象,尽管 typeof null 会输入 object,然而这只是 JS 存在的一个悠久 Bug。在 JS 的最后版本中应用的是 32 位零碎,为了性能思考应用低位存储变量的类型信息,000 结尾代表是对象,然而 null 示意为全零,所以将它谬误的判断为 object。尽管当初的外部类型判断代码曾经扭转了,然而对于这个 Bug 却是始终流传下来。

  • undefined 在 js 中不是一个保留字,这意味着咱们能够应用 undefined 来作为一个变量名,这样的做法是十分危险的,它会影响咱们对 undefined 值的判断。然而咱们能够通过一些办法取得平安的 undefined 值,比如说 void 0
  • 当咱们对两种类型应用 typeof 进行判断的时候,Null 类型化会返回“object”,这是一个历史遗留的问题。当咱们应用双等号对两种类型的值进行比拟时会返回 true,应用三个等号时会返回 false。

meta 标签:主动刷新 / 跳转

假如要实现一个相似 PPT 自动播放的成果,你很可能会想到应用 JavaScript 定时器管制页面跳转来实现。但其实有更加简洁的实现办法,比方通过 meta 标签来实现:

<meta http-equiv="Refresh" content="5; URL=page2.html">

下面的代码会在 5s 之后主动跳转到同域下的 page2.html 页面。咱们要实现 PPT 自动播放的性能,只须要在每个页面的 meta 标签内设置好下一个页面的地址即可。

另一种场景,比方每隔一分钟就须要刷新页面的大屏幕监控,也能够通过 meta 标签来实现,只需去掉前面的 URL 即可:

<meta http-equiv="Refresh" content="60">

meta viewport 相干

<!DOCTYPE html>  <!--H5 规范申明,应用 HTML5 doctype,不辨别大小写 -->
<head lang=”en”> <!-- 规范的 lang 属性写法 -->
<meta charset=’utf-8′>    <!-- 申明文档应用的字符编码 -->
<meta http-equiv=”X-UA-Compatible”content=”IE=edge,chrome=1″/>   <!-- 优先应用 IE 最新版本和 Chrome-->
<meta name=”description”content=”不超过 150 个字符”/>       <!-- 页面形容 -->
<meta name=”keywords”content=””/>     <!-- 页面关键词 -->
<meta name=”author”content=”name, email@gmail.com”/>    <!-- 网页作者 -->
<meta name=”robots”content=”index,follow”/>      <!-- 搜索引擎抓取 -->
<meta name=”viewport”content=”initial-scale=1, maximum-scale=3, minimum-scale=1, user-scalable=no”> <!-- 为挪动设施增加 viewport-->
<meta name=”apple-mobile-web-app-title”content=”题目”> <!--iOS 设施 begin-->
<meta name=”apple-mobile-web-app-capable”content=”yes”/>  <!-- 增加到主屏后的题目(iOS 6 新增)是否启用 WebApp 全屏模式,删除苹果默认的工具栏和菜单栏 -->
<meta name=”apple-itunes-app”content=”app-id=myAppStoreID, affiliate-data=myAffiliateData, app-argument=myURL”>
<!-- 增加智能 App 广告条 Smart App Banner(iOS 6+ Safari)-->
<meta name=”apple-mobile-web-app-status-bar-style”content=”black”/>
<meta name=”format-detection”content=”telphone=no, email=no”/>  <!-- 设置苹果工具栏色彩 -->
<meta name=”renderer”content=”webkit”> <!-- 启用 360 浏览器的极速模式(webkit)-->
<meta http-equiv=”X-UA-Compatible”content=”IE=edge”>     <!-- 防止 IE 应用兼容模式 -->
<meta http-equiv=”Cache-Control”content=”no-siteapp”/>    <!-- 不让百度转码 -->
<meta name=”HandheldFriendly”content=”true”>     <!-- 针对手持设施优化,次要是针对一些老的不辨认 viewport 的浏览器,比方黑莓 -->
<meta name=”MobileOptimized”content=”320″>   <!-- 微软的老式浏览器 -->
<meta name=”screen-orientation”content=”portrait”>   <!--uc 强制竖屏 -->
<meta name=”x5-orientation”content=”portrait”>    <!--QQ 强制竖屏 -->
<meta name=”full-screen”content=”yes”>              <!--UC 强制全屏 -->
<meta name=”x5-fullscreen”content=”true”>       <!--QQ 强制全屏 -->
<meta name=”browsermode”content=”application”>   <!--UC 利用模式 -->
<meta name=”x5-page-mode”content=”app”>   <!-- QQ 利用模式 -->
<meta name=”msapplication-tap-highlight”content=”no”>    <!--windows phone 点击无高亮
设置页面不缓存 -->
<meta http-equiv=”pragma”content=”no-cache”>
<meta http-equiv=”cache-control”content=”no-cache”>
<meta http-equiv=”expires”content=”0″>

参考 前端进阶面试题具体解答

对 WebSocket 的了解

WebSocket 是 HTML5 提供的一种浏览器与服务器进行 全双工通信 的网络技术,属于应用层协定。它基于 TCP 传输协定,并复用 HTTP 的握手通道。浏览器和服务器只须要实现一次握手,两者之间就间接能够创立持久性的连贯,并进行双向数据传输。

WebSocket 的呈现就解决了半双工通信的弊病。它最大的特点是:服务器能够向客户端被动推动音讯,客户端也能够被动向服务器推送音讯。

WebSocket 原理:客户端向 WebSocket 服务器告诉(notify)一个带有所有接收者 ID(recipients IDs)的事件(event),服务器接管后立刻告诉所有沉闷的(active)客户端,只有 ID 在接收者 ID 序列中的客户端才会解决这个事件。

WebSocket 特点的如下:

  • 反对双向通信,实时性更强
  • 能够发送文本,也能够发送二进制数据‘’
  • 建设在 TCP 协定之上,服务端的实现比拟容易
  • 数据格式比拟轻量,性能开销小,通信高效
  • 没有同源限度,客户端能够与任意服务器通信
  • 协定标识符是 ws(如果加密,则为 wss),服务器网址就是 URL
  • 与 HTTP 协定有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采纳 HTTP 协定,因而握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

Websocket 的应用办法如下:

在客户端中:

// 在 index.html 中间接写 WebSocket,设置服务端的端口号为 9999
let ws = new WebSocket('ws://localhost:9999');
// 在客户端与服务端建设连贯后触发
ws.onopen = function() {console.log("Connection open."); 
    ws.send('hello');
};
// 在服务端给客户端发来音讯的时候触发
ws.onmessage = function(res) {console.log(res);       // 打印的是 MessageEvent 对象
    console.log(res.data);  // 打印的是收到的音讯
};
// 在客户端与服务端建设敞开后触发
ws.onclose = function(evt) {console.log("Connection closed.");
}; 

高低垂直居中计划

  • 定高:marginposition + margin(负值)
  • 不定高:position + transformflexIFC + vertical-align:middle
/* 定高计划 1 */
.center {
  height: 100px;
  margin: 50px 0;   
}
/* 定高计划 2 */
.center {
  height: 100px;
  position: absolute;
  top: 50%;
  margin-top: -25px;
}
/* 不定高计划 1 */
.center {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
}
/* 不定高计划 2 */
.wrap {
  display: flex;
  align-items: center;
}
.center {width: 100%;}
/* 不定高计划 3 */
/* 设置 inline-block 则会在外层产生 IFC,高度设为 100% 撑开 wrap 的高度 */
.wrap::before {
  content: '';
  height: 100%;
  display: inline-block;
  vertical-align: middle;
}
.wrap {text-align: center;}
.center {
  display: inline-block;  
  vertical-align: middle;
}

TCP/IP 五层协定

TCP/IP五层协定和 OSI 的七层协定对应关系如下:

  • 应用层 (application layer):间接为利用过程提供服务。应用层协定定义的是利用过程间通信和交互的规定,不同的利用有着不同的应用层协定,如 HTTP 协定(万维网服务)、FTP 协定(文件传输)、SMTP 协定(电子邮件)、DNS(域名查问)等。
  • 传输层 (transport layer):有时也译为运输层,它负责为两台主机中的过程提供通信服务。该层次要有以下两种协定:

    • 传输控制协议 (Transmission Control Protocol,TCP):提供面向连贯的、牢靠的数据传输服务,数据传输的根本单位是报文段(segment);
    • 用户数据报协定 (User Datagram Protocol,UDP):提供无连贯的、尽最大致力的数据传输服务,但不保障数据传输的可靠性,数据传输的根本单位是用户数据报。
  • 网络层 (internet layer):有时也译为网际层,它负责为两台主机提供通信服务,并通过抉择适合的路由将数据传递到指标主机。
  • 数据链路层 (data link layer):负责将网络层交下来的 IP 数据报封装成帧,并在链路的两个相邻节点间传送帧,每一帧都蕴含数据和必要的管制信息(如同步信息、地址信息、差错控制等)。
  • 物理层 (physical Layer):确保数据能够在各种物理媒介上进行传输,为数据的传输提供牢靠的环境。

从上图中能够看出,TCP/IP模型比 OSI 模型更加简洁,它把 应用层 / 表示层 / 会话层 全副整合为了 应用层

在每一层都工作着不同的设施,比方咱们罕用的交换机就工作在数据链路层的,个别的路由器是工作在网络层的。在每一层实现的协定也各不同,即每一层的服务也不同,下图列出了每层次要的传输协定:

同样,TCP/IP五层协定的通信形式也是对等通信:

事件机制

涉及面试题:事件的触发过程是怎么样的?晓得什么是事件代理嘛?

1. 简介

事件流是一个事件沿着特定数据结构流传的过程。冒泡和捕捉是事件流在 DOM 中两种不同的流传办法

事件流有三个阶段

  • 事件捕捉阶段
  • 处于指标阶段
  • 事件冒泡阶段

事件捕捉

事件捕捉(event capturing):艰深的了解就是,当鼠标点击或者触发 dom 事件时,浏览器会从根节点开始由外到内进行事件流传,即点击了子元素,如果父元素通过事件捕捉形式注册了对应的事件的话,会先触发父元素绑定的事件

事件冒泡

事件冒泡(dubbed bubbling):与事件捕捉恰恰相反,事件冒泡程序是由内到外进行事件流传,直到根节点

无论是事件捕捉还是事件冒泡,它们都有一个独特的行为,就是事件流传

2. 捕捉和冒泡

<div id="div1">
  <div id="div2"></div>
</div>

<script>
    let div1 = document.getElementById('div1');
    let div2 = document.getElementById('div2');

    div1.onClick = function(){alert('1')
    }

    div2.onClick = function(){alert('2');
    }

</script>

当点击 div2时,会弹出两个弹出框。在 ie8/9/10chrome浏览器,会先弹出”2”再弹出“1”,这就是事件冒泡:事件从最底层的节点向上冒泡流传。事件捕捉则跟事件冒泡相同

W3C 的规范是先捕捉再冒泡,addEventListener的第三个参数决定把事件注册在捕捉(true)还是冒泡(false)

3. 事件对象

4. 事件流阻止

在一些状况下须要阻止事件流的流传,阻止默认动作的产生

  • event.preventDefault():勾销事件对象的默认动作以及持续流传。
  • event.stopPropagation()/ event.cancelBubble = true:阻止事件冒泡。

事件的阻止在不同浏览器有不同解决

  • IE 下应用 event.returnValue= false
  • 在非 IE 下则应用 event.preventDefault()进行阻止

preventDefault 与 stopPropagation 的区别

  • preventDefault通知浏览器不必执行与事件相关联的默认动作(如表单提交)
  • stopPropagation是进行事件持续冒泡,然而对 IE9 以下的浏览器有效

5. 事件注册

  • 通常咱们应用 addEventListener 注册事件,该函数的第三个参数能够是布尔值,也能够是对象。对于布尔值 useCapture 参数来说,该参数默认值为 falseuseCapture 决定了注册的事件是捕捉事件还是冒泡事件
  • 一般来说,咱们只心愿事件只触发在指标上,这时候能够应用 stopPropagation 来阻止事件的进一步流传。通常咱们认为 stopPropagation 是用来阻止事件冒泡的,其实该函数也能够阻止捕捉事件。stopImmediatePropagation 同样也能实现阻止事件,然而还能阻止该事件指标执行别的注册事件
node.addEventListener('click',(event) =>{event.stopImmediatePropagation()
    console.log('冒泡')
},false);
// 点击 node 只会执行下面的函数,该函数不会执行
node.addEventListener('click',(event) => {console.log('捕捉')
},true)

6. 事件委托

  • js 中性能优化的其中一个次要思维是缩小 dom 操作。
  • 节俭内存
  • 不须要给子节点登记事件

假如有 100li,每个 li 有雷同的点击事件。如果为每 个 Li都增加事件,则会造成 dom 拜访次数过多,引起浏览器重绘与重排的次数过多,性能则会升高。应用事件委托则能够解决这样的问题

原理

实现事件委托是利用了事件的冒泡原理实现的。当咱们为最外层的节点增加点击事件,那么外面的 ullia 的点击事件都会冒泡到最外层节点上,委托它代为执行事件

<ul id="ul">
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ul>
<script>
  window.onload = function(){var ulEle = document.getElementById('ul');
    ul.onclick = function(ev){
        // 兼容 IE
        ev = ev || window.event;
        var target = ev.target || ev.srcElement;

        if(target.nodeName.toLowerCase() == 'li'){alert( target.innerHTML);
        }

    }
  }
</script>

New 的原理

常见考点

  • new 做了那些事?
  • new 返回不同的类型时会有什么体现?
  • 手写 new 的实现过程

new 关键词的 次要作用就是执行一个构造函数、返回一个实例对象,在 new 的过程中,依据构造函数的状况,来确定是否能够承受参数的传递。上面咱们通过一段代码来看一个简略的 new 的例子

function Person(){this.name = 'Jack';}
var p = new Person(); 
console.log(p.name)  // Jack

这段代码比拟容易了解,从输入后果能够看出,p 是一个通过 person 这个构造函数生成的一个实例对象,这个应该很容易了解。

new 操作符能够帮忙咱们构建出一个实例,并且绑定上 this,外部执行步骤可大略分为以下几步:

  1. 创立一个新对象
  2. 对象连接到构造函数原型上,并绑定 this(this 指向新对象)
  3. 执行构造函数代码(为这个新对象增加属性)
  4. 返回新对象

在第四步返回新对象这边有一个状况会例外:

那么问题来了,如果不必 new 这个关键词,联合下面的代码革新一下,去掉 new,会产生什么样的变动呢?咱们再来看上面这段代码

function Person(){this.name = 'Jack';}
var p = Person();
console.log(p) // undefined
console.log(name) // Jack
console.log(p.name) // 'name' of undefined
  • 从下面的代码中能够看到,咱们没有应用 new 这个关键词,返回的后果就是 undefined。其中因为 JavaScript 代码在默认状况下 this 的指向是 window,那么 name 的输入后果就为 Jack,这是一种不存在 new 关键词的状况。
  • 那么当构造函数中有 return 一个对象的操作,后果又会是什么样子呢?咱们再来看一段在下面的根底上革新过的代码。
function Person(){
   this.name = 'Jack'; 
   return {age: 18}
}
var p = new Person(); 
console.log(p)  // {age: 18}
console.log(p.name) // undefined
console.log(p.age) // 18

通过这段代码又能够看出,当构造函数最初 return 进去的是一个和 this 无关的对象时,new 命令会间接返回这个新对象 而不是通过 new 执行步骤生成的 this 对象

然而这里要求构造函数必须是返回一个对象,如果返回的不是对象,那么还是会依照 new 的实现步骤,返回新生成的对象。接下来还是在下面这段代码的根底之上略微改变一下

function Person(){
   this.name = 'Jack'; 
   return 'tom';
}
var p = new Person(); 
console.log(p)  // {name: 'Jack'}
console.log(p.name) // Jack

能够看出,当构造函数中 return 的不是一个对象时,那么它还是会依据 new 关键词的执行逻辑,生成一个新的对象(绑定了最新 this),最初返回进去

因而咱们总结一下:new 关键词执行之后总是会返回一个对象,要么是实例对象,要么是 return 语句指定的对象

手工实现 New 的过程

function create(fn, ...args) {if(typeof fn !== 'function') {throw 'fn must be a function';}
    // 1、用 new Object() 的形式新建了一个对象 obj
  // var obj = new Object()
    // 2、给该对象的__proto__赋值为 fn.prototype,即设置原型链
  // obj.__proto__ = fn.prototype

  // 1、2 步骤合并
  // 创立一个空对象,且这个空对象继承构造函数的 prototype 属性
  // 即实现 obj.__proto__ === constructor.prototype
  var obj = Object.create(fn.prototype);

    // 3、执行 fn,并将 obj 作为外部 this。应用 apply,扭转构造函数 this 的指向到新建的对象,这样 obj 就能够拜访到构造函数中的属性
  var res = fn.apply(obj, args);
    // 4、如果 fn 有返回值,则将其作为 new 操作返回内容,否则返回 obj
    return res instanceof Object ? res : obj;
};
  • 应用 Object.createobj 的 proto 指向为构造函数的原型
  • 应用 apply 办法,将构造函数内的 this 指向为 obj
  • create 返回时,应用三目运算符决定返回后果。

咱们晓得,构造函数如果有显式返回值,且返回值为对象类型,那么构造函数返回后果不再是指标实例

如下代码:

function Person(name) {
  this.name = name
  return {1: 1}
}
const person = new Person(Person, 'lucas')
console.log(person)
// {1: 1}

测试

// 应用 create 代替 new
function Person() {...}
// 应用内置函数 new
var person = new Person(1,2)

// 应用手写的 new,即 create
var person = create(Person, 1,2)

new 被调用后大抵做了哪几件事件

  • 让实例能够拜访到公有属性;
  • 让实例能够拜访构造函数原型(constructor.prototype)所在原型链上的属性;
  • 构造函数返回的最初后果是援用数据类型。

模块化

js 中当初比拟成熟的有四种模块加载计划:

  • 第一种是 CommonJS 计划,它通过 require 来引入模块,通过 module.exports 定义模块的输入接口。这种模块加载计划是服务器端的解决方案,它是以同步的形式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取十分快,所以以同步的形式加载没有问题。但如果是在浏览器端,因为模块的加载是应用网络申请,因而应用异步加载的形式更加适合。
  • 第二种是 AMD 计划,这种计划采纳异步加载的形式来加载模块,模块的加载不影响前面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载实现后再执行回调函数。require.js 实现了 AMD 标准
  • 第三种是 CMD 计划,这种计划和 AMD 计划都是为了解决异步模块加载的问题,sea.js 实现了 CMD 标准。它和 require.js 的区别在于模块定义时对依赖的解决不同和对依赖模块的执行机会的解决不同。
  • 第四种计划是 ES6 提出的计划,应用 import 和 export 的模式来导入导出模块

在有 Babel 的状况下,咱们能够间接应用 ES6的模块化

// file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}

import {a, b} from './a.js'
import XXX from './b.js'

CommonJS

CommonJsNode 独有的标准,浏览器中应用就须要用到 Browserify解析了。

// a.js
module.exports = {a: 1}
// or
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1

在上述代码中,module.exportsexports 很容易混同,让咱们来看看大抵外部实现

var module = require('./a.js')
module.a
// 这里其实就是包装了一层立刻执行函数,这样就不会净化全局变量了,// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {a: 1}
// 根本实现
var module = {exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法类似的起因
var exports = module.exports
var load = function (module) {
    // 导出的货色
    var a = 1
    module.exports = a
    return module.exports
};

再来说说 module.exportsexports,用法其实是类似的,然而不能对 exports 间接赋值,不会有任何成果。

对于 CommonJSES6 中的模块化的两者区别是:

  • 前者反对动静导入,也就是 require(${path}/xx.js),后者目前不反对,然而已有提案, 前者是同步导入,因为用于服务端,文件都在本地,同步导入即便卡住主线程影响也不大。
  • 而后者是异步导入,因为用于浏览器,须要下载文件,如果也采纳同步导入会对渲染有很大影响
  • 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会扭转,所以如果想更新值,必须从新导入一次。
  • 然而后者采纳实时绑定的形式,导入导出的值都指向同一个内存地址,所以导入值会追随导出值变动
  • 后者会编译成 require/exports 来执行的

AMD

AMD 是由 RequireJS 提出的

AMD 和 CMD 标准的区别?

  • 第一个方面是在模块定义时对依赖的解决不同。AMD 推崇依赖前置,在定义模块的时候就要申明其依赖的模块。而 CMD 推崇就近依赖,只有在用到某个模块的时候再去 require。
  • 第二个方面是对依赖模块的执行机会解决不同。首先 AMD 和 CMD 对于模块的加载形式都是异步加载,不过它们的区别在于模块的执行机会,AMD 在依赖模块加载实现后就间接执行依赖模块,依赖模块的执行程序和咱们书写的程序不肯定统一。而 CMD 在依赖模块加载实现后并不执行,只是下载而已,等到所有的依赖模块都加载好后,进入回调函数逻辑,遇到 require 语句的时候才执行对应的模块,这样模块的执行程序就和咱们书写的程序保持一致了。
// CMD
define(function(require, exports, module) {var a = require("./a");
  a.doSomething();
  // 此处略去 100 行
  var b = require("./b"); // 依赖能够就近书写
  b.doSomething();
  // ...
});

// AMD 默认举荐
define(["./a", "./b"], function(a, b) {
  // 依赖必须一开始就写好
  a.doSomething();
  // 此处略去 100 行
  b.doSomething();
  // ...
})
  • AMDrequirejs 在推广过程中对模块定义的规范化产出,提前执行,推崇依赖前置
  • CMDseajs 在推广过程中对模块定义的规范化产出,提早执行,推崇依赖就近
  • CommonJs:模块输入的是一个值的 copy,运行时加载,加载的是一个对象(module.exports 属性),该对象只有在脚本运行完才会生成
  • ES6 Module:模块输入的是一个值的援用,编译时输入接口,ES6模块不是对象,它对外接口只是一种动态定义,在代码动态解析阶段就会生成。

谈谈对模块化开发的了解

  • 我对模块的了解是,一个模块是实现一个特定性能的一组办法。在最开始的时候,js 只实现一些简略的性能,所以并没有模块的概念,但随着程序越来越简单,代码的模块化开发变得越来越重要。
  • 因为函数具备独立作用域的特点,最原始的写法是应用函数来作为模块,几个函数作为一个模块,然而这种形式容易造成全局变量的净化,并且模块间没有分割。
  • 前面提出了对象写法,通过将函数作为一个对象的办法来实现,这样解决了间接应用函数作为模块的一些毛病,然而这种方法会裸露所有的所有的模块成员,内部代码能够批改外部属性的值。
  • 当初最罕用的是立刻执行函数的写法,通过利用闭包来实现模块公有作用域的建设,同时不会对全局作用域造成净化。

TCP 粘包是怎么回事,如何解决?

默认状况下, TCP 连贯会启⽤提早传送算法 (Nagle 算法), 在数据发送之前缓存他们. 如果短时间有多个数据发送, 会缓冲到⼀起作⼀次发送 (缓冲⼤⼩⻅ socket.bufferSize), 这样能够缩小 IO 耗费提⾼性能.

如果是传输⽂件的话, 那么基本不⽤解决粘包的问题, 来⼀个包拼⼀个包就好了。然而如果是多条音讯, 或者是别的⽤途的数据那么就须要解决粘包.

上面看⼀个例⼦, 间断调⽤两次 send 别离发送两段数据 data1 和 data2, 在接收端有以下⼏种常⻅的状况:
A. 先接管到 data1, 而后接管到 data2 .
B. 先接管到 data1 的局部数据, 而后接管到 data1 余下的局部以及 data2 的全副.
C. 先接管到了 data1 的全副数据和 data2 的局部数据, 而后接管到了 data2 的余下的数据.
D. ⼀次性接管到了 data1 和 data2 的全副数据.

其中的 BCD 就是咱们常⻅的粘包的状况. ⽽对于解决粘包的问题, 常⻅的解决⽅案有:

  • 屡次发送之前距离⼀个等待时间:只须要等上⼀段时间再进⾏下⼀次 send 就好, 适⽤于交互频率特地低的场景. 毛病也很显著, 对于⽐较频繁的场景⽽⾔传输效率切实太低,不过⼏乎不⽤做什么解决.
  • 敞开 Nagle 算法:敞开 Nagle 算法, 在 Node.js 中你能够通过 socket.setNoDelay() ⽅法来敞开 Nagle 算法, 让每⼀次 send 都不缓冲间接发送。该⽅法⽐较适⽤于每次发送的数据都⽐较⼤ (但不是⽂件那么⼤), 并且频率不是特地⾼的场景。如果是每次发送的数据量⽐较⼩, 并且频率特地⾼的, 敞开 Nagle 纯属⾃废文治。另外, 该⽅法不适⽤于⽹络较差的状况, 因为 Nagle 算法是在服务端进⾏的包合并状况, 然而如果短时间内客户端的⽹络状况不好, 或者应⽤层因为某些起因不能及时将 TCP 的数据 recv, 就会造成多个包在客户端缓冲从⽽粘包的状况。(如果是在稳固的机房外部通信那么这个概率是⽐较⼩能够抉择疏忽的)
  • 进⾏封包 / 拆包: 封包 / 拆包是⽬前业内常⻅的解决⽅案了。即给每个数据包在发送之前, 于其前 / 后放⼀些有特色的数据, 而后收到数据的时 候依据特色数据宰割进去各个数据包。

Proxy 代理

proxy 在指标对象的外层搭建了一层拦挡,外界对指标对象的某些操作,必须通过这层拦挡

var proxy = new Proxy(target, handler);

new Proxy()示意生成一个 Proxy 实例,target参数示意所要拦挡的指标对象,handler参数也是一个对象,用来定制拦挡行为

var target = {name: 'poetries'};
 var logHandler = {get: function(target, key) {console.log(`${key} 被读取 `);
     return target[key];
   },
   set: function(target, key, value) {console.log(`${key} 被设置为 ${value}`);
     target[key] = value;
   }
 }
 var targetWithLog = new Proxy(target, logHandler);

 targetWithLog.name; // 控制台输入:name 被读取
 targetWithLog.name = 'others'; // 控制台输入:name 被设置为 others

 console.log(target.name); // 控制台输入: others
  • targetWithLog 读取属性的值时,实际上执行的是 logHandler.get:在控制台输入信息,并且读取被代理对象 target 的属性。
  • targetWithLog 设置属性值时,实际上执行的是 logHandler.set:在控制台输入信息,并且设置被代理对象 target 的属性的值
// 因为拦挡函数总是返回 35,所以拜访任何属性都失去 35
var proxy = new Proxy({}, {get: function(target, property) {return 35;}
});

proxy.time // 35
proxy.name // 35
proxy.title // 35

Proxy 实例也能够作为其余对象的原型对象

var proxy = new Proxy({}, {get: function(target, property) {return 35;}
});

let obj = Object.create(proxy);
obj.time // 35

proxy对象是 obj 对象的原型,obj对象自身并没有 time 属性,所以依据原型链,会在 proxy 对象上读取该属性,导致被拦挡

Proxy 的作用

对于代理模式 Proxy 的作用次要体现在三个方面

  • 拦挡和监督内部对对象的拜访
  • 升高函数或类的复杂度
  • 在简单操作前对操作进行校验或对所需资源进行治理

Proxy 所能代理的范畴 –handler

实际上 handler 自身就是 ES6 所新设计的一个对象. 它的作用就是用来 自定义代理对象的各种可代理操作。它自身一共有 13 中办法, 每种办法都能够代理一种操作. 其 13 种办法如下

// 在读取代理对象的原型时触发该操作,比方在执行 Object.getPrototypeOf(proxy) 时。handler.getPrototypeOf()

// 在设置代理对象的原型时触发该操作,比方在执行 Object.setPrototypeOf(proxy, null) 时。handler.setPrototypeOf()


// 在判断一个代理对象是否是可扩大时触发该操作,比方在执行 Object.isExtensible(proxy) 时。handler.isExtensible()


// 在让一个代理对象不可扩大时触发该操作,比方在执行 Object.preventExtensions(proxy) 时。handler.preventExtensions()

// 在获取代理对象某个属性的属性形容时触发该操作,比方在执行 Object.getOwnPropertyDescriptor(proxy, "foo") 时。handler.getOwnPropertyDescriptor()


// 在定义代理对象某个属性时的属性形容时触发该操作,比方在执行 Object.defineProperty(proxy, "foo", {}) 时。andler.defineProperty()


// 在判断代理对象是否领有某个属性时触发该操作,比方在执行 "foo" in proxy 时。handler.has()

// 在读取代理对象的某个属性时触发该操作,比方在执行 proxy.foo 时。handler.get()


// 在给代理对象的某个属性赋值时触发该操作,比方在执行 proxy.foo = 1 时。handler.set()

// 在删除代理对象的某个属性时触发该操作,比方在执行 delete proxy.foo 时。handler.deleteProperty()

// 在获取代理对象的所有属性键时触发该操作,比方在执行 Object.getOwnPropertyNames(proxy) 时。handler.ownKeys()

// 在调用一个指标对象为函数的代理对象时触发该操作,比方在执行 proxy() 时。handler.apply()


// 在给一个指标对象为构造函数的代理对象结构实例时触发该操作,比方在执行 new proxy() 时。handler.construct()

为何 Proxy 不能被 Polyfill

  • 如 class 能够用 function 模仿;promise能够用 callback 模仿
  • 然而 proxy 不能用 Object.defineProperty 模仿

目前谷歌的 polyfill 只能实现局部的性能,如 get、set https://github.com/GoogleChro…

// commonJS require
const proxyPolyfill = require('proxy-polyfill/src/proxy')();

// Your environment may also support transparent rewriting of commonJS to ES6:
import ProxyPolyfillBuilder from 'proxy-polyfill/src/proxy';
const proxyPolyfill = ProxyPolyfillBuilder();

// Then use...
const myProxy = new proxyPolyfill(...);

从输出 URL 到页面展现过程

1. DNS 域名解析

  • 根 DNS 服务器:返回顶级域 DNS 服务器的 IP 地址
  • 顶级域 DNS 服务器:返回权威 DNS 服务器的 IP 地址
  • 权威 DNS 服务器:返回相应主机的 IP 地址

DNS 的域名查找,在客户端和浏览器,本地 DNS 之间的查问形式是递归查问;在本地 DNS 服务器与根域及其子域之间的查问形式是迭代查问;

[外链图片转存失败, 源站可能有防盗链机制, 倡议将图片保留下来间接上传(img-jssOTrgb-1671415246034)(null)]

在客户端输出 URL 后,会有一个递归查找的过程,从 浏览器缓存中查找 -> 本地的 hosts 文件查找 -> 找本地 DNS 解析器缓存查找 -> 本地 DNS 服务器查找,这个过程中任何一步找到了都会完结查找流程。

如果本地 DNS 服务器无奈查问到,则依据本地 DNS 服务器设置的转发器进行查问。若未用转发模式,则迭代查找过程如下图:

联合起来的过程,能够用一个图示意:

在查找过程中,有以下优化点:

  • DNS 存在着多级缓存,从离浏览器的间隔排序的话,有以下几种: 浏览器缓存,零碎缓存,路由器缓存,IPS 服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存
  • 在域名和 IP 的映射过程中,给了利用基于域名做负载平衡的机会,能够是简略的负载平衡,也能够依据地址和运营商做全局的负载平衡。

2. 建设 TCP 连贯

首先,判断是不是 https 的,如果是,则 HTTPS 其实是 HTTP + SSL / TLS 两局部组成,也就是在 HTTP 上又加了一层解决加密信息的模块。服务端和客户端的信息传输都会通过 TLS 进行加密,所以传输的数据都是加密后的数据

进行三次握手,建设 TCP 连贯。

  • 第一次握手:建设连贯。客户端发送连贯申请报文段
  • 第二次握手:服务器收到 SYN 报文段。服务器收到客户端的 SYN 报文段,须要对这个 SYN 报文段进行确认
  • 第三次握手:客户端收到服务器的 SYN+ACK 报文段,向服务器发送 ACK 报文段

SSL 握手过程

  • 第一阶段 建设平安能力 包含协定版本 会话 Id 明码构件 压缩办法和初始随机数
  • 第二阶段 服务器发送证书 密钥替换数据和证书申请,最初发送申请 - 相应阶段的完结信号
  • 第三阶段 如果有证书申请客户端发送此证书 之后客户端发送密钥替换数据 也能够发送证书验证音讯
  • 第四阶段 变更明码构件和完结握手协定

实现了之后,客户端和服务器端就能够开始传送数据

发送 HTTP 申请,服务器解决申请,返回响应后果

TCP 连贯建设后,浏览器就能够利用 HTTP/HTTPS 协定向服务器发送申请了。服务器承受到申请,就解析申请头,如果头部有缓存相干信息如if-none-match 与 if-modified-since,则验证缓存是否无效,若无效则返回状态码为304,若有效则从新返回资源,状态码为200

这里有产生的一个过程是 HTTP 缓存,是一个常考的考点,大抵过程如图:

3. 敞开 TCP 连贯

4. 浏览器渲染

依照渲染的工夫程序,流水线可分为如下几个子阶段:构建 DOM 树、款式计算、布局阶段、分层、栅格化和显示。如图:

  • 渲染过程将 HTML 内容转换为可能读懂 DOM 树结构。
  • 渲染引擎将 CSS 样式表转化为浏览器能够了解的 styleSheets,计算出 DOM 节点的款式。
  • 创立布局树,并计算元素的布局信息。
  • 对布局树进行分层,并生成分层树。
  • 为每个图层生成绘制列表,并将其提交到合成线程。合成线程将图层分图块,并栅格化将图块转换成位图。
  • 合成线程发送绘制图块命令给浏览器过程。浏览器过程依据指令生成页面,并显示到显示器上。

构建 DOM 树

  • 转码(Bytes -> Characters)—— 读取接管到的 HTML 二进制数据,按指定编码格局将字节转换为 HTML 字符串
  • Tokens 化(Characters -> Tokens)—— 解析 HTML,将 HTML 字符串转换为构造清晰的 Tokens,每个 Token 都有非凡的含意同时有本人的一套规定
  • 构建 Nodes(Tokens -> Nodes)—— 每个 Node 都增加特定的属性(或属性拜访器),通过指针可能确定 Node 的父、子、兄弟关系和所属 treeScope(例如:iframe 的 treeScope 与外层页面的 treeScope 不同)
  • 构建 DOM 树(Nodes -> DOM Tree)—— 最重要的工作是建设起每个结点的父子兄弟关系

款式计算

渲染引擎将 CSS 样式表转化为浏览器能够了解的 styleSheets,计算出 DOM 节点的款式。

CSS 款式起源次要有 3 种,别离是通过 link 援用的内部 CSS 文件、style 标签内的 CSS、元素的 style 属性内嵌的 CSS。

页面布局

布局过程,即 排除 script、meta 等功能化、非视觉节点,排除 display: none 的节点,计算元素的地位信息,确定元素的地位,构建一棵只蕴含可见元素布局树。如图:

[外链图片转存失败, 源站可能有防盗链机制, 倡议将图片保留下来间接上传(img-XPEAM1L1-1671415246222)(null)]

其中,这个过程须要留神的是回流和重绘

生成分层树

页面中有很多简单的成果,如一些简单的 3D 变换、页面滚动,或者应用 z-indexing 做 z 轴排序等,为了更加不便地实现这些成果,渲染引擎还须要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)

栅格化

合成线程会依照视口左近的图块来优先生成位图,理论生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图

通常一个页面可能很大,然而用户只能看到其中的一部分,咱们把用户能够看到的这个局部叫做视口(viewport)。在有些状况下,有的图层能够很大,比方有的页面你应用滚动条要滚动良久能力滚动到底部,然而通过视口,用户只能看到页面的很小一部分,所以在这种状况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。

显示

最初,合成线程发送绘制图块命令给浏览器过程。浏览器过程依据指令生成页面,并显示到显示器上,渲染过程实现。

右边定宽,左边自适应计划

float + margin,float + calc

/* 计划 1 */ 
.left {
  width: 120px;
  float: left;
}
.right {margin-left: 120px;}
/* 计划 2 */ 
.left {
  width: 120px;
  float: left;
}
.right {width: calc(100% - 120px);
  float: left;
}

浏览器存储

咱们常常须要对业务中的一些数据进行存储,通常能够分为 短暂性存储 和 持久性贮存。

  • 短暂性的时候,咱们只须要将数据存在内存中,只在运行时可用
  • 持久性存储,能够分为 浏览器端 与 服务器端

    • 浏览器:

      • cookie: 通常用于存储用户身份,登录状态等

        • http 中主动携带,体积下限为 4K,可自行设置过期工夫
      • localStorage / sessionStorage: 短暂贮存 / 窗口敞开删除,体积限度为 4~5M
      • indexDB
    • 服务器:

      • 分布式缓存 redis
      • 数据库

cookie 和 localSrorage、session、indexDB 的区别

个性 cookie localStorage sessionStorage indexDB
数据生命周期 个别由服务器生成,能够设置过期工夫 除非被清理,否则始终存在 页面敞开就清理 除非被清理,否则始终存在
数据存储大小 4K 5M 5M 有限
与服务端通信 每次都会携带在 header 中,对于申请性能影响 不参加 不参加 不参加

从上表能够看到,cookie 曾经不倡议用于存储。如果没有大量数据存储需要的话,能够应用 localStoragesessionStorage。对于不怎么扭转的数据尽量应用 localStorage 存储,否则能够用 sessionStorage 存储。

对于 cookie,咱们还须要留神安全性

属性 作用
value 如果用于保留用户登录态,应该将该值加密,不能应用明文的用户标识
http-only 不能通过 JS拜访 Cookie,缩小 XSS攻打
secure 只能在协定为 HTTPS 的申请中携带
same-site 规定浏览器不能在跨域申请中携带 Cookie,缩小 CSRF 攻打
  • Name,即该 Cookie 的名称。Cookie 一旦创立,名称便不可更改。
  • Value,即该 Cookie 的值。如果值为 Unicode 字符,须要为字符编码。如果值为二进制数据,则须要应用 BASE64 编码。
  • Max Age,即该 Cookie 生效的工夫,单位秒,也常和 Expires 一起应用,通过它能够计算出其无效工夫。Max Age如果为负数,则该 CookieMax Age 秒之后生效。如果为正数,则敞开浏览器时 Cookie 即生效,浏览器也不会以任何模式保留该 Cookie
  • Path,即该 Cookie 的应用门路。如果设置为 /path/,则只有门路为 /path/ 的页面能够拜访该 Cookie。如果设置为 /,则本域名下的所有页面都能够拜访该 Cookie
  • Domain,即能够拜访该 Cookie 的域名。例如如果设置为 .zhihu.com,则所有以 zhihu.com,结尾的域名都能够拜访该 CookieSize 字段,即此 Cookie 的大小。
  • Http 字段,即 Cookiehttponly 属性。若此属性为 true,则只有在 HTTP Headers 中会带有此 Cookie 的信息,而不能通过 document.cookie 来拜访此 Cookie。
  • Secure,即该 Cookie 是否仅被应用平安协定传输。平安协定。平安协定有 HTTPS、SSL 等,在网络上传输数据之前先将数据加密。默认为 false

为什么 udp 不会粘包?

  • TCP 协定是⾯向流的协定,UDP 是⾯向音讯的协定。UDP 段都是⼀条音讯,应⽤程序必须以音讯为单位提取数据,不能⼀次提取任意字节的数据
  • UDP 具备爱护音讯边界,在每个 UDP 包中就有了音讯头(消息来源地址,端⼝等信息),这样对于接收端来说就容易进⾏辨别解决了。传输协定把数据当作⼀条独⽴的音讯在⽹上传输,接收端只能接管独⽴的音讯。接收端⼀次只能接管发送端收回的⼀个数据包, 如果⼀次承受数据的⼤⼩⼩于发送端⼀次发送的数据⼤⼩,就会失落⼀局部数据,即便失落,承受端也不会分两次去接管。

如何高效操作 DOM

1. 为什么说 DOM 操作耗时

1.1 线程切换

  • 浏览器为了防止两个引擎同时批改页面而造成渲染后果不统一的状况,减少了另外一个机制,这 两个引擎具备互斥性 ,也就是说在某个时刻只有 一个引擎在运行,另一个引擎会被阻塞。操作系统在进行线程切换的时候须要保留上一个线程执行时的状态信息并读取下一个线程的状态信息,俗称上下文切换。而这个操作相对而言是比拟耗时的
  • 每次 DOM 操作就会引发线程的上下文切换——从 JavaScript 引擎切换到渲染引擎执行对应操作,而后再切换回 JavaScript 引擎继续执行,这就带来了性能损耗。单次切换耗费的工夫是非常少的,然而如果频繁地大量切换,那么就会产生性能问题

比方上面的测试代码,循环读取一百万次 DOM 中的 body 元素的耗时是读取 JSON 对象耗时的 10 倍。

// 测试次数:一百万次
const times = 1000000
// 缓存 body 元素
console.time('object')
let body = document.body
// 循环赋值对象作为对照参考
for(let i=0;i<times;i++) {let tmp = body}
console.timeEnd('object')// object: 1.77197265625ms

console.time('dom')
// 循环读取 body 元素引发线程切换
for(let i=0;i<times;i++) {let tmp = document.body}
console.timeEnd('dom')// dom: 18.302001953125ms

1.2 从新渲染

另一个更加耗时的因素是元素及款式变动引起的再次渲染,在 渲染过程中最耗时的两个步骤为重排(Reflow)与重绘(Repaint)

浏览器在渲染页面时会将 HTML 和 CSS 别离解析成 DOM 树和 CSSOM 树,而后合并进行排布,再绘制成咱们可见的页面。如果在操作 DOM 时波及到元素、款式的批改,就会引起渲染引擎从新计算款式生成 CSSOM 树,同时还有可能触发对元素的从新排布和从新绘制

  • 可能会影响到其余元素排布的操作就会引起重排,继而引发重绘

    • 批改元素边距、大小
    • 增加、删除元素
    • 扭转窗口大小
  • 引起重绘

    • 设置背景图片
    • 批改字体色彩
    • 扭转 visibility属性值

理解更多对于重绘和重排的款式属性,能够参看这个网址:https://csstriggers.com/ (opens new window)

2. 如何高效操作 DOM

明确了 DOM 操作耗时之后,要晋升性能就变得很简略了,反其道而行之,缩小这些操作即可

2.1 在循环外操作元素

比方上面两段测试代码比照了读取 1000 次 JSON 对象以及拜访 1000 次 body 元素的耗时差别,相差一个数量级

const times = 10000;
console.time('switch')
for (let i = 0; i < times; i++) {document.body === 1 ? console.log(1) : void 0;
}
console.timeEnd('switch') // 1.873046875ms
var body = JSON.stringify(document.body)
console.time('batch')
for (let i = 0; i < times; i++) {body === 1 ? console.log(1) : void 0;
}
console.timeEnd('batch') // 0.846923828125ms

2.2 批量操作元素

比如说要创立 1 万个 div 元素,在循环中间接创立再增加到父元素上耗时会十分多。如果采纳字符串拼接的模式,先将 1 万个 div 元素的 html 字符串拼接成一个残缺字符串,而后赋值给 body 元素的 innerHTML 属性就能够显著缩小耗时

const times = 10000;
console.time('createElement')
for (let i = 0; i < times; i++) {const div = document.createElement('div')
  document.body.appendChild(div)
}
console.timeEnd('createElement')// 54.964111328125ms
console.time('innerHTML')
let html=''
for (let i = 0; i < times; i++) {html+='<div></div>'}
document.body.innerHTML += html // 31.919921875ms
console.timeEnd('innerHTML')

ES6 之前应用 prototype 实现继承

Object.create() 会创立一个“新”对象,而后将此对象外部的 [[Prototype]] 关联到你指定的对象(Foo.prototype)。Object.create(null) 创立一个空 [[Prototype]] 链接的对象,这个对象无奈进行委托。

function Foo(name) {this.name = name;}

Foo.prototype.myName = function () {return this.name;}

// 继承属性,通过借用结构函数调用
function Bar(name, label) {Foo.call(this, name);
  this.label = label;
}

// 继承办法,创立备份
Bar.prototype = Object.create(Foo.prototype);

// 必须设置回正确的构造函数,要不然在会产生判断类型出错
Bar.prototype.constructor = Bar;

 // 必须在上一步之后
Bar.prototype.myLabel = function () {return this.label;}

var a = new Bar("a", "obj a");

a.myName(); // "a"
a.myLabel(); // "obj a"
正文完
 0