js基础知识

214 阅读23分钟

JavaScript 相关

数组降维

  1. 简单递归

    function reduceDimension(arr){
        let ret = [];
        let toArr = function(arr){
            arr.forEach(function(item){
                item instanceof Array ? toArr(item) : ret.push(item);
            });
        }
        toArr(arr);
        return ret;
    }
    
  2. flat()

    arr.flat(),默认参数为1,可以扁平化二维数组,当参数为n 时,扁平化n+1维数组;也就是说为0时,返回新的一位数组;Infinity,可以返回任意深度的嵌套数组;

    var arr1 = [1, 2, [3, 4]];
    arr1.flat(); 
    // [1, 2, 3, 4]
     
    var arr2 = [1, 2, [3, 4, [5, 6]]];
    arr2.flat();
    // [1, 2, 3, 4, [5, 6]]
     
    var arr3 = [1, 2, [3, 4, [5, 6]]];
    arr3.flat(2);
    // [1, 2, 3, 4, 5, 6]
     
    //使用 Infinity 作为深度,展开任意深度的嵌套数组
    arr3.flat(Infinity); 
    // [1, 2, 3, 4, 5, 6]
    
  3. 无递归操作

    // 不使用递归,使用 stack 无限反嵌套多层嵌套数组
    var arr1 = [1,2,3,[1,2,3,4, [2,3,4]]];
    function flatten(input) {
      const stack = [...input];
      const res = [];
      while (stack.length) {
        // 使用 pop 从 stack 中取出并移除值
        const next = stack.pop();
        if (Array.isArray(next)) {
          // 使用 push 送回内层数组中的元素,不会改动原始输入 original input
          stack.push(...next);
        } else {
          res.push(next);
        }
      }
      // 使用 reverse 恢复原数组的顺序
      return res.reverse();
    }
    flatten(arr1);// [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
    
  4. reduce() concat() 以及递归

    function flatten(arr) {
    	return arr.reduce((acc,item) => {
    		Array.isArray(item) ?  acc.concat(flatten(item)): acc.concat(item);
    	},[])
    }
    

类数组转化为数组

  1. Array.from(arraylike)
  2. Array.apply(null,arraylike)
  3. Array.prototype.concat.apply([],arraylike)
  4. [...arraylike]

声明变量var let const

  1. var

    • var 声明的变量有函数作用域的概念,但是没有块级作用域的概念

    • var 可以重复声明已存在的变量 ,后者会覆盖前者

    • var 声明的变量会有变量提升

      var liList = document.querySelectorAll('li') // 共5li
      for( var i=0; i<liList.length; i++){
      liList[i].onclick = function(){
        console.log(i)
      }
      }
      //共55
      

    var a =1; var a =2; console.log(a) // 2

    console.log(a) // undefined var a = 1;

    
    
    
  2. let

    • let 声明的变量具有块级作用域

    • let 不能重复声明已存在的变量

    • let 有暂时死区,不会被提升

      var liList = document.querySelectorAll('li') // 共5个li
      for( let i=0; i<liList.length; i++){
      liList[i].onclick = function(){
        console.log(i)
      }
      }
      //0,1,2,3,4
      

    let a =1; let a =2; //SyntaxError: Identifier 'a' has already been declared

    console.log(a) // ReferenceError: a is not defined let a = 1;

    
    let的深刻认识
    
  3. const

    • const 声明一个只读的常量。一旦声明,常量的值就不能改变。(const 声明的变量不得改变值,这意味着, const 一旦声明变量,就必须立即初始化,不能留到以后赋值。同也不能重复声明)

    • const 声明的变量有块级作用域

    • 不存在变量提升

      const tem = 1;
      tem = 2;
      console.log(tem) //TypeError: Assignment to constant variable.
      -----------------------------------------------------------
      const tem; //SyntaxError: Missing initializer in const declaration
      -----------------------------------------------------------
      const 实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在
      变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针, const 只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
      const tem = { foo: 'liyuqing' };
      tem.foo = 'caoyang';
      console.log(tem) //{ foo: 'caoyang' }
      
  4. es5实现const

    const PI = 3.141593
    -----------------------------------------
    Object.defineProperty(typeof global === "object" ? global : window, "PI", {
        value:        3.141593,
        enumerable:   true,
        writable:     false,
        configurable: false
    })
    
    configurable 控制是否可以删除 writable 控制是否可以修改(赋值)
    当然 enumerable 控制是否可以枚举.
    

js基础类型

  1. 7种基础类型
    • Number,
    • String
    • Boolean
    • null
    • Undefined
    • Object(一般对象,特殊对象Array,Function等等)
    • Symbol(es6新增)

typeof & instanceof

  1. typeof 可判断Number,String, Boolean,Undefined,Symbol,function,Object(null也为object,array也是object)

    • typeof 1

    • typeof null

      作者看过JS引擎代码,得出结论。参考《The history of “typeof null”》
      大致意思如下:第一版的JavaScript是用32位比特来存储值的,且是通过值的低1位或3位来识别类型的(有点哈法曼编码的味道)。
      1:整型(int000:引用类型(object010:双精度浮点型(double100:字符串(string110:布尔型(boolean)
      另外还用两个特殊值:
      undefined,用整数−2^30(负230次方,不在整型的范围内)
      null,机器码空指针(C/C++ 宏定义),低三位也是000
      
      作者:贝叶斯
      链接:https://www.zhihu.com/question/21691758/answer/1118265190
      来源:知乎
      
    • typeof new Error

    • typeof Error

    • typeof NaN

  2. instanceof 判断构造方法的prototype对象是否出现在实例的原型对象上

    • arr instaceof Array
  3. 判断Array的方法

  • Array.isArray(arr)
  • arr instanceof Array
  1. 万能方法

    Object.prototype.toString.call()

    Object.prototype.toString.call(new Array)

    Object.prototype.toString.call(new Date)

    Object.prototype.toString.call(null)  <!--"[object Null]"-->
    
  2. instanceof 简易实现

    function instance_of(i,c){
        let  pro = c.prototype;
        let  inst = i.__proto__;
        while(1){
            if(inst === null) return false;
            if(inst === pro) return true;
            inst = inst.__proto__;
        }
    }
    var arr =[];
    console.log(instance_of(arr,Object))
    

数组常用方法

  1. foreEach((item,index,arr) => {}) 遍历数组,无返回值,不改变原数组
  2. filter((item,index,arr) => {}) 返回过滤后的新数组,不改变原数组
  3. map((item,index,arr) => {}) 回调函数体内需要return, 返回map后的新数组,不改变原数组
  4. concat([]||item) 返回连接后的新数组,不改变原数组
  5. reduce((acc,item) => {})
  6. every((item,index,arr) => {}) 判断每一项是不是都满足条件,满足的话返回true
  7. some((item,index,arr) => {}) 判断是不是有满足条件的项,满足的话返回true
  8. pop(),push() pop() 返回删除的项 改变原数组
  9. shift(),unshift() shift() 返回删除的项 改变原数组
  10. slice(idx,n) 返回截取的项,不改变原数组(从下标为idx的项开始截取,截取到第n项(即对应n-1的下标项))
  11. splice(idx, num, item...) 从下标为idx的项开始删除,删除包括这一项以及之后的num项,拼接item。返回删除的项,改变原数组
  12. join() 将数组转化为字符串,对应string.split()
  13. toSting()

循环遍历数组

  1. for() 循环,通过下标访问每一项,较为麻烦

  2. arr.forEach() 可以获得每项以及下标值,但是回调函数中不能使用await ,会报错

  3. for/in 一般用来遍历对象,数组是特殊的对象,属性值为number类型,所以非数值的数组下标,也会被遍历到,通过下标访问

  4. for/of es6新增(最优解),直接访问每一项,若想访问下标,可以使用arr.entries()

    for (const [index, element] of arr.entries()){
        console.log(index, element);
    }
    

循环遍历对象

  1. for/in
  2. for/of obj. keys()
  3. for/of obj.entries()

浅拷贝 & 深拷贝

  1. 浅拷贝&深拷贝的区别

为什么基本数据类型保存在栈中,而引用数据类型保存在堆中? 1)堆比栈大,栈比堆速度快; 2)基本数据类型比较稳定,而且相对来说占用的内存小; 3)引用数据类型大小是动态的,而且是无限的,引用值的大小会改变,不能把它放在栈中,否则会降低变量查找的速度,因此放在变量栈空间的值是该对象存储在堆中的地址,地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响; 4)堆内存是无序存储,可以根据引用直接获取;

浅拷贝和深拷贝都只针对于引用数据类型,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存;但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象;

区别:浅拷贝只复制对象的第一层属性、深拷贝可以对对象的属性进行递归复值,创建对象的副本;

  1. 浅拷贝深拷贝的实现方式

    • 浅拷贝的实现方式(只能实现一层的简单拷贝)

      1. 新建一个空对象,遍历原对象,实现键值的复制

        // 只复制第一层的浅拷贝
        function simpleCopy(obj1) {
           var obj2 = Array.isArray(obj1) ? [] : {};
           for (let i in obj1) {
           obj2[i] = obj1[i];
          }
           return obj2;
        }
        var obj1 = {
           a: 1,
           b: 2,
           c: {
           d: 3
          }
        }
        var obj2 = simpleCopy(obj1);
        obj2.a = 3;
        obj2.c.d = 4;
        alert(obj1.a); // 1
        alert(obj2.a); // 3
        alert(obj1.c.d); // 4
        alert(obj2.c.d); // 4
        
      2. 使用Object.assign({},obj) 效果同上

      3. 使用Object.create(obj) 效果同上

      • 深拷贝的实现方式

      1. 递归实现,为了避免相互引用导致死循环,改良版

        function deepCopy(obj) {
               //声明一个变量用来储存拷贝之后的内容
               let newobj = null;   
        
              //判断数据类型是否是复杂类型,如果是则调用自己,再次循环,如果不是,直接赋值即可,
          	  //由于null不可以循环但类型又是object,所以这个需要对null进行判断
              if (typeof (obj) == 'object' && obj !== null) {
                //声明一个变量用以储存拷贝出来的值,根据参数的具体数据类型声明不同的类型来储存
                newobj = obj instanceof Array ? [] : {};
                //循环obj 中的每一项,如果里面还有复杂数据类型,则直接利用递归再次调用copy函数
                for (var i in obj) {
                  newobj[i] = deepCopy(obj[i])
                }
              } else {
                newobj = obj
              }
              return newobj;    //函数必须有返回值,否则结构为undefined
        }
            
            var obj1 = {
              a: 1,
              b: 2,
              c: {
                d: 3
              }
            }
            var obj2 = deepCopy(obj1);
            obj2.a = 3;
            obj2.c.d = 4;
            alert(obj1.a); // 1
            alert(obj2.a); // 3
            alert(obj1.c.d); // 3
            alert(obj2.c.d); // 4
        
      2. 使用JSON.stringify和JSON.parse实现深拷贝:JSON.stringify把对象转成字符串,再用JSON.parse把字符串转成新的对象;

      缺陷: 这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,也就是说,只有可以转成JSON格式的对象才可以这样用,像function没办法转成JSON;

  2. 热门的函数库lodash,也有提供_.cloneDeep用来做深拷贝;

  3. jquery 提供一个$.extend可以用来做深拷贝;

       var $ = require('jquery');
    var obj1 = {
          a: 1,
          b: {
            f: {
              g: 1
            }
          },
          c: [1, 2, 3]
       };
       var obj2 = $.extend(true, {}, obj1);
       console.log(obj1.b.f === obj2.b.f);  // false
    
  4. 注意数组的slice() concat()都不是深拷贝

数组去重

  1. 利用es6 set去重
function unique (arr) {
	return Array.from(new Set(arr))
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]//暂时无法去除空对象

简化版
[...new Set(arr)] //Set结构也有迭代器接口
  1. 朴素的双重循环

    function unique(arr){
        for(var i=0; i<arr.length; i++){
            for(var j=i+1; j<arr.length; j++){
                if(arr[i]==arr[j]){ //第一个等同于第二个,splice方法删除第二个
                    arr.splice(j,1);
                    j--;
                }
            }
        }
        return arr;
    }
    var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
    console.log(unique(arr))
    //[1, "true", 15, false, undefined, NaN, NaN, "NaN", "a", {…}, {…}] //NaN和{}没有去重,两个null直接消失了
    
  2. 通过判断要去重的数组每一项在暂存数组里有没有相同的值去重

    • arr.includes()

    • reduce() +includes()/indexof()-

      function unique(arr){
      	return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]);
      }
      var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
      console.log(unique(arr));
      // [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]
      
    • indexOf()

      function unique(arr) {
          if (!Array.isArray(arr)) {
              console.log('type error!')
              return
          }
          var array = [];
          for (var i = 0; i < arr.length; i++) {
              if (array .indexOf(arr[i]) === -1) {
              	array .push(arr[i])
              }
          }
          return array;
      }
      var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
      console.log(unique(arr))
      // [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}] //NaN、{}没有去重
      
  3. 通过filter () 与 indexOf() 实现去重

function unique(arr) {
    return arr.filter(function(item, index, arr) {
    	//当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
    	return arr.indexOf(item, 0) === index;
    });
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, "NaN", 0, "a", {…}, {…}]
  1. 利用sort() 去重

    function unique(arr) {
        if (!Array.isArray(arr)) {
            console.log('type error!')
            return;
        }
        arr = arr.sort()
        var arrry= [arr[0]];
            for (var i = 1; i < arr.length; i++) {
                if (arr[i] !== arr[i-1]) {
                	arrry.push(arr[i]);
            }
        }
        return arrry;
    }
    var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
    console.log(unique(arr))
    // [0, 1, 15, "NaN", NaN, NaN, {…}, {…}, "a", false, null, true, "true", undefined] //NaN、{}没有去重
    
  2. 利用filter() hasOwnProperty()去重,需要新增一个暂存对象,原理同3,只不过换成通过判断自定义的key

    function unique(arr) {
        var obj = {};
        return arr.filter(function(item, index, arr){
        	return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + 	item] = true)
        })
    }
    var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
    console.log(unique(arr))
    //[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}] 
    //所有的都去重了
    
  3. 使用Map数据结构去重

    function unique(arr) {
    let map = new Map();
    let array = new Array(); // 数组用于返回结果
    for (let i = 0; i < arr.length; i++) {
    if(map .has(arr[i])) { // 如果有该key值
    map .set(arr[i], true);
    } else {
    map .set(arr[i], false); // 如果没有该key值
    array .push(arr[i]);
    }
    }
    return array ;
    }
    var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
    console.log(unique(arr))
    

commonjs & requirejs(AMD) & es6 import

  1. 模块化

    • 解决耦合性
    • 代码便于查找
    • 减少污染全局环境
  2. commonjs

    • 每个js文件都可当做一个模块,在服务器端,模块的加载是同步加载的,在浏览器端,需要编译打包处理
    • 暴露module.exports = {} exports. * = value 引入 require
    • 服务器端实现 node 浏览器端实现 browserify
  3. AMD

    • AMD专门用于浏览器端,模块加载是异步的

    • 暴露 无依赖的模块 define(function(){ return 需要暴漏的模块}) 暴露有依赖的模块 define(['mod1','mod2'],function(m1,m2){ return 需要暴漏的模块}) 引用requirejs(['mod1','mod2'],function(m1,m2){ 操作 })

    • 实现 Require.js

      html
      <script date-main="./main.js" src="./require.js"></script>
      main.js
      requirejs.config({
      	baseurl: 对应查找js文件路径的根路径
      	path: {
      		'依赖模块名': 对应js文件路径
      	}
      })
      
      
  4. es6 import

    • 依赖模块需要编译打包处理

    • 暴露和引用模块 import export

      //统一暴露
      function f1(){}
      function f2 (){}
      export {f1,f2}
      //分别暴露
      export function f1(){}
      export function f1(){}
      以上两种方式,import时需用到解构赋值 
      import {f1,f2} from ***
      //默认暴露,可以暴露任意类型,export default  在文件中只能使用一次
      export default () => {}
      import moddefault from ***
      
    • 使用babel browserify

  5. CMD(了解)

    • CMD专门用于浏览器端,也是异步加载,什么时候使用什么时候加载

    • 暴露模块与引用模块

      //暴露模块
      define(function(require,exports,module){
      	//同步引入
      	var mod1 = require('./module1')
      	//异步引入
      	require.async('./module2',function(mod2){
      		//读取完执行回调函数,mod2作为形参传入
      	})
      	
      })
      //引入模块
      define(function(require){
      	var mod1 = require('./module1')
      	mod1.show();
      })
      
    • 实现sea.js

      <script src="./sea.js"></script>
      <script>
      	seajs.use('./main.js')
      </script>
      

Promise

  1. promise 介绍

    • promise是一种异步解决方案,最先由社区提出,后面被引入es6标准,原生提供promise对象。promise使得异步操作通过同步流程表现出来,避免了层层嵌套的回调函数,更加合理,使得操作更加容易。
    • 两个特点
      1. promise,承诺,promise对象有三种状态,pending,fulfilled,rejected,状态变更只能由pending->fulfilled或者pending-> rejected,状态的变更只受异步操作的结果的影响,任何其他操作都无法改变这个状态。
      2. 状态一旦改变,就不会再变,任何时候都可以得到这个结果。状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。
    • 缺陷
      1. 只要新建一个promise就会立即执行,不能取消
      2. pending到fulfilled,rejected的进度无法得知
      3. promise内部的错误要是不设置回调的话无法反映到外部
  2. 简单手写promise原理

    const Promise = function(){
    	this.msg = '' // 存放value和error
        this.status = 'pending'
        var that = this
        var process = arguments[0]
      
        process (function () {
          that.status = 'fulfilled'
          that.msg = arguments[0]
        }, function () {
          that.status = 'rejected'
          that.msg = arguments[0]
        })
        return this
    }
    Promise.prototype.then = function(){
    	if (this.status === 'fulfilled') {
          arguments[0](this.msg)
        } else if (this.status === 'rejected' && arguments[1]) {
          arguments[1](this.msg)
        }
    }
    
  3. 利用promise封装Ajax请求

    var getJson  = function(url){
        const promise = new Promise((resolve.reject) => {
            function handle(){
            	if(this.readyState !== 4) return
            	if(this.status === 200){
            		resolve(this.response)
            	}else {
            		reject(new Error(this.statusText))
            	}
            }
    		var XHR = new XMLHttpRequest();
    		XHR.open("get",url);
    		XHR.responseType = "json";
    		XHR.onreadystatechange = handle;
    		Xhr.send();
        })
        return promise;
    }
    getJson('http://****').then( res => {
    	console.log(res)
    }, error => {
    	console.log(error)
    })
    
  4. promise 表现

    • 基本用法

      1. new Promise()
      const promise = new Promise(function(resolve, reject) {
      // ... some code
      if (/* 异步操作成功 */){
          	resolve(value);
          } else {
          	reject(error);
          }
      });
      promise.then(function(value) {
          // success
          }, function(error) {
          // failure
      });//第二个参数可选
      

      Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject 。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

      1. Promise.prototype.then()

      2. Promise.prototype.catch()

        Promise.prototype.catch 方法是 .then(null, rejection) 的别名,用于指定发生错误时的回调函数。

        另外, then 方法指定的回调函数,如果运行中抛出错误,也会被 catch 方法捕获。也就是说promise以及then方法的报错都能捕获到,不像then 的rejected 方法,只能捕获到promise的报错

      ​ Promise 在 resolve 语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦

      ​ 改变,就永久保持该状态,不会再变了。

      1. Promise.all()

        Promise.all 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

        const p = Promise.all([p1, p2, p3]);
        

        上面代码中, Promise.all 方法接受一个数组作为参数, p1 、 p2 、 p3 都是 Promise 实例,如果不是,就会先调用下面讲到的 Promise.resolve 方法,将参数转为 Promise 实例,再进一步处理。( Promise.all 方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。) p 的状态由 p1 、 p2 、 p3 决定,分成两种情况。 (1)只有 p1 、 p2 、 p3 的状态都变成 fulfilled , p 的状态才会变成 fulfilled ,此时 p1 、 p2 、 p3 的返回值组成一个数组,传递给 p 的回调函数。 (2)只要 p1 、 p2 、 p3 之中有一个被 rejected , p 的状态就变成 rejected ,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数。

      2. Promise.race()

        const p = Promise.race([p1, p2, p3]);
        

        上面代码中,只要 p1 、 p2 、 p3 之中有一个实例率先改变状态, p 的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 p 的回调函 数。

      3. Promise.resolve()

        有时需要将现有对象转为 Promise 对象, Promise.resolve 方法就起到这个作用。resolve的参数作为异步操作结果返回给then

        等价于 new Promise(resolve => resolve('foo'))

      4. Promise.reject()

        等价于 new Promise((resolve, reject) => reject('出错了'))

      5. 自己扩展方法,不管异步操作结果是什么,最后都要执行

        Promise.prototype.finally = function (callback) {
            let P = this.constructor;
            return this.then(
                value => P.resolve(callback()).then(() => value),
                reason => P.resolve(callback()).then(() => { throw reason })
            );
        };
        

Generator & async函数

  1. Generator 介绍

    Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator 函数是一个状态机,封装了多个内部状态,执行 Generator 函数会返回一个遍历器对象。

    形式上,Generator 函数是一个普通函数,但是有两个特征。一是, function 关键字与函数名之间有一个星号;二是,函数体内部使用 yield 表达式,定义不同的内部状态( yield 在英语里的意思就是“产出”)。

    内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式(或 return 语句)为止。换言之,Generator 函数是分段执行的, yield 表达式是暂停执行的标记,而 next 方法可以恢复执行。

    function* helloWorldGenerator() {
        yield 'hello';
        yield 'world';
        return 'ending';
    }
    var hw = helloWorldGenerator();
    hw.next()
    // { value: 'hello', done: false }
    hw.next()
    // { value: 'world', done: false }
    hw.next()
    // { value: 'ending', done: true }
    hw.next()
    // { value: undefined, done: true }
    

    遇到 yield 表达式,就暂停执行后面的操作,并将紧跟在 yield 后面的那个表达式的值,作为返回的对象的 value 属性值。yield 后面的表达式 123 + 456 ,不会立即求值,只会在 next 方法将指针移到这一句时,才会求值。

  2. 与Iterator 的关系

    任意一个对象的 Symbol.iterator 方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的 Symbol.iterator 属性,从而使得该对象具有 Iterator 接口。

    var myIterable = {};
    myIterable[Symbol.iterator] = function* () {
        yield 1;
        yield 2;
        yield 3;
    };
    [...myIterable] // [1, 2, 3]
    
  3. Generator的异步任务的封装

    • 异步任务的简单封装

      var fetch = require('node-fetch');
      function* gen(){
          var url = 'https://api.github.com/users/github';
          var result = yield fetch(url);
          console.log(result.bio);
      }
      var g = gen();
      var result = g.next();
      result.value.then(function(data){
           	return data.json();
          }).then(function(data){
           	g.next(data);
      });
      //流程管理极不方便
      
    • Thunk 函数

      Thunk 函数是自动执行 Generator 函数的一种方法。

      1. thunk函数的含义

      编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。

      function f(m) {
      	return m * 2;
      }
      f(x + 5);
      // 等同于
      var thunk = function () {
      	return x + 5;
      };
      function f(thunk) {
      	return thunk() * 2;
      }
      
      1. JavaScript 语言的 Thunk 函数

        JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是将多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。

        // 正常版本的readFile(多参数版本)
        fs.readFile(fileName, callback);
        // Thunk版本的readFile(单参数版本)
        var Thunk = function (fileName) {
            return function (callback) {
            	return fs.readFile(fileName, callback);
            };
        };
        var readFileThunk = Thunk(fileName);
        readFileThunk(callback);
        
      2. Genertor 与 thunk函数

        yield 命令用于将程序的执行权移出 Generator 函数,那么就需要一种方法,将执行权再交还给 Generator 函数。这种方法就是 Thunk 函数,因为它可以在回调函数里,将执行权交还给 Generator 函数。

        function run(fn) {
        var gen = fn();
        function next(err, data) {
            var result = gen.next(data);
            if (result.done) return;
            	result.value(next);
            }
            next();
        }
        function* g() {
        // ...
        }
        run(g);
        

        上面代码的 run 函数,就是一个 Generator 函数的自动执行器。内部的 next 函数就是 Thunk 的回调函数。 next 函数先将指针移到 Generator 函数的下一步( gen.next 方法),然后判断 Generator 函数是否结束( result.done 属性),如果没结束,就将 next 函数再传入 Thunk 函数( result.value 属性),否则就直接退出。 有了这个执行器,执行 Generator 函数方便多了。不管内部有多少个异步操作,直接把 Generator 函数传入 run 函数即可。当然,前提是每一个异步操作,都要是 Thunk 函数,也就是说,跟在 yield 命令后面的必须是 Thunk 函数。

        Thunk 函数并不是 Generator 函数自动执行的唯一方案。因为自动执行的关键是,必须有一种机制,自动控制 Generator 函数的流程,接收和交还程序的执行权。回调函数可以做到这一点,Promise 对象也可以做到这一点。

  4. Generator的异步应用 & async函数

    一比较就会发现, async 函数就是将 Generator 函数的星号( * )替换成 async ,将 yield 替换成 await ,仅此而已。

    async 是对Generator的改进,表现为:

    • 内置执行器; async 函数的执行,与普通函数一模一样,只要一行。
    • 更好的语义;async 和 await ,比起星号和 yield ,语义更清楚了。 async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
    • 更广的适用性;co 模块约定, yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
    • 返回值是 Promise;进一步说, async 函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而 await 命令就是内部 then 命令的语法糖。

async promise setTimeout 执行顺序

  1. promise.Trick()>promise的回调>async>setTimeout>setImmediate
async function asyn1(){
    console.log('async1 start')
    await asyn2()
    console.log('async1 end')
}
async function asyn2(){
    console.log('async2')
}
console.log('all start')
setTimeout(function(){
    console.log('settimeout')
},1000);
asyn1();
new Promise(function(resolve,reject){
    resolve()
}).then(function(){
    console.log('promise4')
})
new Promise(function(resolve,reject){
    console.log('promise1')
    resolve()
    console.log('promise2')
}).then(function(){
    console.log('promise3')
    throw new Error('errors')
}).catch(function(){
    console.log('this is error')
})
console.log('all end')

// all start
// async1 start
// async2
// promise1
// promise2
// all end
// promise4
// promise3
// this is error
// async1 end
// settimeout

JSONP

  1. JSONP介绍

    JSONP(JSON with Padding)是JSON的一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题。由于同源策略,一般来说位于 server1.example.com 的网页无法与不是 server1.example.com的服务器沟通,而 HTML 的

  2. JSONP实现

    • 在客户端调用提供JSONP支持的URL Service,获取JSONP格式数据。

      比如客户想访问www.yiwuku.com/myService.a…

      假设客户期望返回JSON数据:["customername1","customername2"]

      那么真正返回到客户端的Script Tags: callbackFunction([“customername1","customername2"])

    • 完整code实现

      <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
      <html xmlns="http://www.w3.org/1999/xhtml">
      <head>
          <title>Top Customers with Callback</title>
      </head>
      <body>
          <div id="divCustomers"></div>
          <script type="text/javascript">
              function onCustomerLoaded(result, methodName) {
                  var html = '<ul>';
                  for (var i = 0; i < result.length; i++) {
                      html += '<li>' + result[i] + '</li>';
                  }
                  html += '</ul>';
                  document.getElementById('divCustomers').innerHTML = html;
              }
          </script>
          <script type="text/javascript" 
          	src="http://www.yiwuku.com/myService.aspx?jsonp=onCustomerLoaded">
          </script>
      </body>
      </html>
      

前端跨域解决方案

  1. 浏览器同源策略

    所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

    URL 说明 是否允许通信

    www.domain.com/a.js www.domain.com/b.js www.domain.com/lab/c.js

    同一域名,不同文件或路径 允许

    www.domain.com:8000/a.js www.domain.com/b.js

    同一域名,不同端口 不允许

    www.domain.com/a.js www.domain.com/b.js

    同一域名,不同协议 不允许

    www.domain.com/a.js http://192.168.4.12/b.js

    域名和域名对应相同ip 不允许

    www.domain.com/a.js x.domain.com/b.js domain.com/c.js

    主域相同,子域不同 不允许

    www.domain1.com/a.js www.domain2.com/b.js

    不同域名 不允许

  2. 跨域解决方案

    • JSONP (缺点,只能实现一种get请求)

      1. 原生实现如上所示

      2. jquery Ajax实现

        $.ajax({
            url: 'http://www.domain2.com:8080/login',
            type: 'get',
            dataType: 'jsonp',  // 请求方式为jsonp
            jsonpCallback: "handleCallback",  // 自定义回调函数名
            data: {}
        });
        
      3. axios实现

        this.$http = axios;
        this.$http.jsonp('http://www.domain2.com:8080/login', {
            params: {},
            jsonp: 'handleCallback'
        }).then((res) => {
            console.log(res); 
        })
        
    • 跨域资源共享(CORS)

    • nginx代理跨域(原理同CORS)

    • node中间层代理跨域(基本同nginx)

      //设置所有域名跨域访问
      app.all('*', (req, res, next) => {
        res.header("Access-Control-Allow-Origin", "*");
        res.header("Access-Control-Allow-Headers", "X-Requested-With");
        res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
        res.header("X-Powered-By",' 3.2.1')
        res.header("Content-Type", "application/json;charset=utf-8");
        next();
      });
      
    • document.domain + iframe

      只能解决主域相同,子域不同的情况

      父窗口(www.domain.com/a.html)

      <iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
      <script>
          document.domain = 'domain.com';
          var user = 'admin';
      </script>
      

      子窗口(child.domain.com/a.html)

      <script>
          document.domain = 'domain.com';
          // 获取父窗口中变量
          console.log('get js data from parent ---> ' + window.parent.user);
      </script>
      
    • location.hash + iframe

    • window.name + iframe

    • postMessage

    • WebSocekt跨域

      前端代码实现

      <div>user input:<input type="text"></div>
      <script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
      <script>
      var socket = io('http://www.domain2.com:8080');
      
      // 连接成功处理
      socket.on('connect', function() {
          // 监听服务端消息
          socket.on('message', function(msg) {
              console.log('data from server: ---> ' + msg); 
          });
      
          // 监听服务端关闭
          socket.on('disconnect', function() { 
              console.log('Server socket has closed.'); 
          });
      });
      
      document.getElementsByTagName('input')[0].onblur = function() {
          socket.send(this.value);
      };
      </script>
      

      node 端实现

      var http = require('http');
      var socket = require('socket.io');
      
      // 启http服务
      var server = http.createServer(function(req, res) {
          res.writeHead(200, {
              'Content-type': 'text/html'
          });
          res.end();
      });
      
      server.listen('8080');
      console.log('Server is running at port 8080...');
      
      // 监听socket连接
      socket.listen(server).on('connection', function(client) {
          // 接收信息
          client.on('message', function(msg) {
              client.send('hello:' + msg);
              console.log('data from client: ---> ' + msg);
          });
      
          // 断开处理
          client.on('disconnect', function() {
              console.log('Client socket has closed.'); 
          });
      });
      
  3. 总结

以上就是9种常见的跨域解决方案,jsonp(只支持get请求,支持老的IE浏览器)适合加载不同域名的js、css,img等静态资源;CORS(支持所有类型的HTTP请求,但浏览器IE10以下不支持)适合做ajax各种跨域请求;Nginx代理跨域和nodejs中间件跨域原理都相似,都是搭建一个服务器,直接在服务器端请求HTTP接口,这适合前后端分离的前端项目调后端接口。document.domain+iframe适合主域名相同,子域名不同的跨域请求。postMessage、websocket都是HTML5新特性,兼容性不是很好,只适用于主流浏览器和IE10+。

Object.create() 实现

  1. 将传入的对象作为原型

    function create(obj){
    	var fn = function(){};
    	fn.prototype = obj;
    	return new fn();
    }
    

节流和防抖

  1. 函数节流是指一定时间内js方法只跑一次。比如人的眨眼睛,就是一定时间内眨一次。这是函数节流最形象的解释。

    // 函数节流   滚动条滚动
    var canRun = true;
    document.getElementById("throttle").onscroll = function(){   
    	if(!canRun){        
    		// 判断是否已空闲,如果在执行中,则直接return        
    		return;    
    	}
        canRun = false;    
        setTimeout(function(){        
        	console.log("函数节流");        
        	canRun = true;    
        }, 300);
    };
    
    //更解耦  自我实现
    function log1i() {
      console.log(1)
    }
    function throttle(fnli, delay) {
      var canRun = false
      var flag = false
      return function() {
        if (!canRun && flag) return
        if (!flag) {
          flag = true
          fnli()
          canRun = true
        } else {
          canRun = false
          setTimeout(() => {
            fnli()
            canRun = true
          }, delay)
        }
      }
    }
    
    var fn = throttle(log1i, 2000)
    for (let i = 0; i < 10; i++) {
      setTimeout(() => {
        fn()
      }, 1000 * i)
    }
    
    
    //模板 节流
    var throttle = function throttle (action, delay, context, iselapsed) {
        var timeout = null;
        var lastRun = 0;
        return function () {
            if (timeout) {
                if (iselapsed) {
                    return;
                } else {
                    clearTimeout(timeout);
                    timeout = null;
                }
            }
            var elapsed = Date.now() - lastRun;
            var args = arguments;
            if (iselapsed && elapsed >= delay) {
                runCallback();
            } else {
                timeout = setTimeout(runCallback, delay);
            }
            /**
             * 执行回调
             */
            function runCallback() {
                lastRun = Date.now();
                timeout = false;
                action.apply(context, args);
            }
        };
    };
    
    module.exports = {
        "throttle": throttle,
    };
    
    
  2. 函数防抖是指频繁触发的情况下,只有足够的空闲时间,才执行代码一次。比如生活中的坐公交,就是一定时间内,如果有人陆续刷卡上车,司机就不会开车。只有别人没刷卡了,司机才开车。

    // 函数防抖
    var timer = false;
    document.getElementById("debounce").onscroll = function(){    
    	clearTimeout(timer); // 清除未执行的代码,重置回初始化状态
        timer = setTimeout(function(){        
        	console.log("函数防抖");    
        }, 300);
     };
    

原型链 & 继承

  1. 原型链

每个(构造)函数都有一个原型对象prototype,每个原型对象prototype里都有一个指向构造函数的指针,也就是 constructor对象,每个构造函数的实例对象都有一个隐式的指向原型对象的指针,即__proto__对象,所以当通过实例对象去查找一些属性,或者执行属性方法时,会先在实例对象上找,若没有,则通过proto对象查找;这就构成了原型链;

var o = {
           a: 1
       }
console.log(o.toString());
//以上,对象o里没有toString方法,因此会通过o.__proto__查找,这个即指向原型对象(o.__proto__保存的是Object.prototype对象的地址)

o.__proto__.showname = function(){
	console.log('liyuqing')
}
console.dir(o)
var obj ={}
console.log(obj.showname())//任何新建的对象都能执行showname,一般不这样操作,为了便于理解
//以上,修改了__proto__对象,相当于修改了Object.prototype对象

函数的prototype && _proto_

当函数作为构造函数时,实例对象调用的是prototype对象上的方法;

当函数就是简单的变量,函数名调用的是__proto__对象上的方法,即函数的构造函数的prototype对象上的方法,例如fn.apply

for(const key in obj) 会遍历本身的对象以及原型链上的对象,所以一般遍历对象的属性要这样写

for (const key in obj){
	if(obj.hasOwnPrototype(key)){
		//do something
	}
}
  1. 继承

    改变构造函数的原型并不是继承;people.prototype = admin.prototype;这种形式相当于一个构造函数,不能保留各个构造函数的私有功能。

    继承是原型的继承(admin继承people)(几种写法)

    admin.prototype._proto_ = people.prototype;

    admin.prototype = Obejct.create(people.prototype);

    admin.prototype = new people;

    Object.setPrototypeOf(admin, new people)

    //原型继承
    function Sup(name){
        this.name = name;
    }
    Sup.prototype.show = function(){
        console.log(this.name)
    }
    function Sub(name){
        this.name = name
    }
    Sub.prototype = Object.create(Sup.prototype)
    //Sub.prototype = new Sup();//使用new 也可
    let sub = new Sub('lisi')
    sub.show()   //lisi
    注意点:
        (1)给子类原型添加方法一定要放在子类替换原型的语句之后(替换原型相当于重新指向一个地址,所以在这         之前的子类原型的修改是无效的)
        (2)在通过原型链实现继承时,不能使用对象字面量创建原型方法(字面量相当于重新指向一个新地址)
    缺陷: 
    	(1)子类型的所有实例都可以共享父类型的属性 ?(大致理解为原型对象上的属性)
        (2)子类型的实例无法在不影响所有对象的情况下,给父类型的构造函数传递参数
        (3)子类型new出来的对象的构造函数显示为父类;
        ***********************************************************************************
     //构造方法继承
    function Sup(age){
        this.name = ['liyuqing','caoyang'];
        this.age = age
    }
    function Sub(age){
        Sup.call(this,age);
    }
    let sub = new Sub(18);
    sub.name.push('lisi')
    console.log(sub.name)//[ 'liyuqing', 'caoyang', 'lisi' ]
    console.log(sub.age)//18
    
    let sub1 = new Sub(20)
    console.log(sub1.name)//[ 'liyuqing', 'caoyang' ]
    console.log(sub1.age)//20
    缺陷: 
    	(1)方法都在构造函数中定义,函数复用变得没有意义
    补充: 但是可以传参了
        ***********************************************************************************
    
    //组合继承
    基本思想:使用原型链实现对原型属性和方法的继承(主要想继承方法),而通过借用构造函数来实现对实例属性的继承(子类型的实例内部存在同名属性,从而对父类型的同名属性进行屏蔽);最后同时避免了原型链继承时会共享同一个父类型属性和借用构造函数的函数复用的缺陷
    function Sup(name){
        this.name = name
    }
    Sup.prototype.show = function(){
        console.log(this.name)
    }
    function Sub(name){
        Sup.call(this,name)
    }
    Sub.prototype = Object.create(Sup.prototype)
    let sub = new Sub(['yqli'])
    sub.name.push('lisi')
    console.log(sub.show())
    
    let sub1 = new Sub(['yqli'])
    console.log(sub1.show())
    
        ***********************************************************************************
    //寄生继承
      function createOne(sup){
        const instance = Object.create(sup);
        return instance;
    }
    let people = {
        'name': 'yqli',
        'age': 18,
        'show': function(){
        	console.log(this.name)
        }
    }
    let instance = createOne(people)
    instance.name= 'caoyang'
    console.log(instance.show())//caoyang
    
      ***********************************************************************************
    //组合寄生继承
    function extend(sub,sup){
        sub.prototype = Object.create(sup.prototype)
        Object.defineProperty(sub.prototype,constructor,{
            enumerable: false,
            value: sub
        })
    }
    function Sup(name){
        this.name = name
    }
    Sup.prototype.show = function(){
        console.log(this.name)
    }
    function Sub(name){
        Sup.call(this,name)
    }
    extend(Sub,Sup)
    
    let sub = new Sub('lisi');
    sub.show()
        ***********************************************************************************
    //es6 继承
    
  2. es5 继承与es6继承的区别

    es6 class理解,class可以看做语法糖

    function Point(x, y) {
        this.x = x;
        this.y = y;
    }
    Point.prototype.toString = function () {
    	return '(' + this.x + ', ' + this.y + ')';
    };
    var p = new Point(1, 2);
    
    等价于以下类的写法
    class Point{
    	constructor(x,y) {
    		this.x = x;
    		this.y = y;
    	}
    	toString(){
    		return '(' + this.x + ', ' + this.y + ')';
    	}
    }
    //注意点: point紧跟花括号, 类里面的方法之间不需要逗号相隔,相当于方法constructor,toString都定义在Point.prototype上,Point为构造函数,类里面的方法不可枚举
    类不存在变量提升(hoist),这一点与 ES5 完全不同。
        new Foo(); // ReferenceError
        class Foo {}
        
    采用 Class 表达式,可以写出立即执行的 Class。
    let person = new class {
        constructor(name) {
        	this.name = name;
    	}
        sayName() {
            console.log(this.name);
        }
    }('张三');
    person.sayName(); // "张三"
    
    class Point {
    	constructor(x, y) {
            this.x = x;
            this.y = y;
    	}
    }
    class ColorPoint extends Point {
    	constructor(x, y, color) {
            super(x, y); // 调用父类的constructor(x, y)
            this.color = color;
    	}
        toString() {
        	return this.color + ' ' + super.toString(); // 调用父类的toString()
        }
    }
    子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进
    行加工。如果不调用 super 方法,子类就得不到 this 对象。
    

    ES5 的继承,实质是先创造子类的实例对象 this ,然后再将父类的方法添加到 this 上面( Parent.apply(this) )。

    ES6 的继承机制完全不同,实质是先创造父类的实例对象 this (所以必须先调用 super 方法),然后再用子类的构造函数修改 this 。因此 super() 在这里相当于Point.prototype.constructor.call(this) 。

  3. 改变一个对象的原型

    • let newObj = Object.create(obj) // newObj的原型就是obj
    • newObj._proto_ = obj // newObj的原型就是obj,这不是js标准,为一些浏览器厂商的标准
    • Object.setPrototypeOf(newObj, obj) // newObj的原型就是obj 相应的获取原型的方法是Object.getPrototypeOf(newObj)
  4. 有意思的一些操作

    //使用call或者apply  借用原型链
     let obj1 = {
         data: [1,2,4,5,23,4,8,56]
     };
    
    Object.setPrototypeOf(obj1,{
        max: function(){
        	return this.data.sort((a,b) => b-a)[0]
        }
    });
    
    console.log(obj1.max());
    
    let obj2 = {
        newData: [3,4,6,79,97,43,3],
        get data(){
        	return this.newData
    	}
    };
    console.log(obj1.max.apply(obj2))
    
    //不合理的构造方法声明(同一个show方法,在不同实例中都有开辟内存,但其实执行的操作是一样的)
    function user (name){
    	this.name = name;
    	this.show = function(){
    		console.log(this.name)
    	}
    }
    let li = new user('lisi');
    let zha = new user('zhangsan');
    console.log(li)
    console.log(zha)
    //合理的构造方法声明
    function user (name){
    	this.name = name;
    }
    user.prototype.show = function(){
    	console.log(this.name)
    }
    let li = new user('lisi');
    let zha = new user('zhangsan');
    console.log(li)
    console.log(zha)
    

执行上下文(context)

JavaScript执行上下文(context)主要指代码执行环境的抽象概念。执行上下文分为三种:

  • 全局执行上下文
  • 函数执行上下文
  • eval执行上下文

每一段js代码执行,都会先创建一个上下文环境。

箭头函数this指向

  1. this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

  2. 示例

    let obj ={
        name: 'lisi',
        foo: () => {
            console.log(this.name);
        },
    }
    obj.foo()
    //undefined
    在此例中show函数为箭头函数,因此this绑定父级中的this,父级是一个顶级对象,在浏览器中this指向window对象,window中并没有属性name,所以输出undefined
    
    let obj ={
        name: 'lisi',
        foo: function(){
            return () => {
                console.log(this.name);
            }
        },
    }l
    obj.foo()()
    //lisi
    此例中,箭头函数为一匿名函数,其父级作用域为foo函数,因此箭头函数绑定foo函数的作用域,foo函数中this指向调用foo函数的对象,即obj,obj中有name属性,且值为'lisi ',输出'lisi'
    

WebSocket

  1. 诞生原因

    初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?

    答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。

    举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。

    img

    这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。

    轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。

  2. 简介

    WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。

    它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

    img

    其他特点包括:

    (1)建立在 TCP 协议之上,服务器端的实现比较容易。

    (2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

    (3)数据格式比较轻量,性能开销小,通信高效。

    (4)可以发送文本,也可以发送二进制数据。

    (5)没有同源限制,客户端可以与任意服务器通信。

    (6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

    img

    ws://example.com:80/some/path
    
  3. websocket 客户端,服务端实现

    //客户端实现
     $("#add").addEventListener('click', function () {
          let value = $('#text').value;
          let time = video.currentTime;
          let color = $("#color").value;
          let fontSize = $('#range').value;
          let obj = { time, value, color, fontSize };
          socket.send(JSON.stringify(obj))
          // canvasBarrage.add(obj)
        })
    let socket = new WebSocket('ws://localhost:3000');
        socket.onopen = function () {
          socket.onmessage = function (e) {
            let message = JSON.parse(e.data);
            if (message.type === 'ADD') {
              canvasBarrage.add(message)
            } else if (message.type === 'INIT') {
              canvasBarrage = new CanvasBarrage(canvas, video, { data: message.data })
            }
          }
        }
    //服务端实现
    const webSocket = require('ws')
    const redis = require('redis')
    let client = redis.createClient();
    
    let wss = new webSocket.Server({ port: 3000 })
    let clientsArr = []
    wss.on('connection', function (ws) {
      console.log('connection success')
      clientsArr.push(ws)
      //lrange  取得指定下标下的数据,0表示第一个元素,-1表示最后一个元素
      client.lrange('barrages', 0, -1, function (err, applies) {
        //item rpush的时候是
        applies = applies.map(item => JSON.parse(item))
        ws.send(JSON.stringify({
          type: 'INIT',
          data: applies
        }))
      })
      ws.on('message', function (data) {
        // client.rpush('barrages', data, redis.print);
        console.log(data)
        client.rpush('barrages', data);
        clientsArr.forEach(w => {
          ws.send(JSON.stringify({ type: 'ADD', data: JSON.parse(data) }));
        })
      });
      ws.on('close', function () {
        clientsArr = clientsArr.filter(client => client != ws)
      })
    })
    
  4. 参考链接

    阮一峰的网络日志:www.ruanyifeng.com/blog/2017/0…

js 0.1+0.2问题

  1. 表现

    console.log(0.1+0.2) => 0.30000000000000004

  2. 原因

    JS的基础类型Number,遵循 IEEE 754 规范,采用双精度64位存储(double precision),占用 64 bit。如图

    img

    • 1位用来表示符号位(正数的符号位0、负数的符号位为1)
    • 11位用来表示指数
    • 52位表示尾数

    十进制小数转换为二进制小数:用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,此时0或1为二进制的最后一位。或者达到所要求的精度为止。

    如:0.7=(0.1 0110 0110...)B
    0.7*2=1.4========取出整数部分1
    0.4*2=0.8========取出整数部分0
    0.8*2=1.6========取出整数部分1
    0.6*2=1.2========取出整数部分1
    0.2*2=0.4========取出整数部分0
    0.4*2=0.8========取出整数部分0
    0.8*2=1.6========取出整数部分1
    0.6*2=1.2========取出整数部分1
    0.2*2=0.4========取出整数部分0
    
     // 0.1 转化为二进制
    0.0 0011 0011 0011 0011...(0011无限循环)
    
    // 0.2 转化为二进制
    0.0011 0011 0011 0011 0011...(0011无限循环)
    

    由于尾数只有52位,所以对于0.1和0.2转换后的二进制如下:

    e = -4; m =1.1001100110011001100110011001100110011001100110011010 (52位)
    e = -3; m =1.1001100110011001100110011001100110011001100110011010 (52位)
    
    像十进制数有4舍5入的规则一样,二进制也存在类似的规则,简单的说,如果 1.101 
    要保留一位小数,可能的值是 1.1 和 1.2,那么先看 1.101 和 1.1 或者 1.2 哪个值更
    接近,毫无疑问是 1.1,于是答案是 1.1。那么如果要保留两位小数呢?很显然要么
    是 1.10 要么是 1.11,而且又一样近,这时就要看这两个数哪个是偶数(末位是偶
    数),保留偶数为答案。综上,如果第 52 bit 和 53 bit 都是 1,那么是要进位的。
    这也导致了误差的产生。
    

    我们看下这两个二进制相加

      e = -4; m = 1.1001100110011001100110011001100110011001100110011010 (52位)
    + e = -3; m = 1.1001100110011001100110011001100110011001100110011010 (52位)
    ---------------------------------------------------------------------------
    相加时如果指数不一致,需要对齐,一般情况下是向右移,因为最右边的即使溢出了,损失的精度远远小于左边溢出。
      e = -3; m = 0.1100110011001100110011001100110011001100110011001101 
    + e = -3; m = 1.1001100110011001100110011001100110011001100110011010
    ---------------------------------------------------------------------------
      e = -3; m = 10.0110011001100110011001100110011001100110011001100111
    ---------------------------------------------------------------------------
      e = -2; m = 1.0011001100110011001100110011001100110011001100110100(52位)
    ---------------------------------------------------------------------------
    = 0.010011001100110011001100110011001100110011001100110100
    = 0.30000000000000004(十进制)
    

    我们可以看到,当十进制小数的二进制表示的有限数字超过 52 位时,在 JavaScript 里是不能精确存储的,这时候就存在舍入误差(Round-off error)。

  3. 如何解决

    对于整数,前端出现问题的几率可能比较低,毕竟很少有业务需要需要用到超大整数,只要运算结果不超过 Math.pow(2, 53) 就不会丢失精度。

    对于小数,前端出现问题的几率还是很多的,尤其在一些电商网站涉及到金额等数据。解决方式:把小数放到位整数(乘倍数),再缩小回原来倍数(除倍数)

  4. 参考链接

    www.cnblogs.com/kefeiGame/p…

    www.jianshu.com/p/90ce596f1…

setTimeout & for

  1. 表现

    for (var i = 0; i < 5; i++) { 
    	setTimeout(function (){
    		console.log(i);  
    	 },1000);  
    }
    结果:5 5 5 5 5(注意最后的i++)
    

    为什么不是 1 2 3 4 5,问题出在作用域上。

    因为 setTimeout 的 console.log(i); 的i是 var 定义的,所以是函数级的作用域,不属于 for 循环体,属于 global。等到 for 循环结束,i 已经等于 5 了,这个时候再执行 setTimeout 的五个回调函数(参考上面对事件机制的阐述),里面的 console.log(i); 的 i 去向上找作用域,只能找到 global下 的 i,即 5。所以输出都是 5。

  2. 解决方法

    解决办法:人为给 console.log(i); 创造作用域,保存i的值。

    for (var i = 0; i < 5; i++) { 
     	(function(i){      //立刻执行函数
    		setTimeout(function (){
    			console.log(i);  
    		 },1000);  
     	})(i);	
    }
    
    for (let i = 0; i < 5; i++) {     //let 代替 var
    	setTimeout(function (){
    		console.log(i);  
    	 },1000);  
    }
    

    let 为代码块的作用域,所以每一次 for 循环,console.log(i); 都引用到 for 代码块作用域下的i,因为这样被引用,所以 for 循环结束后,这些作用域在 setTimeout 未执行前都不会被释放。

new 操作符

  1. new操作符新建了一个空对象,这个对象原型指向构造函数的prototype,执行构造函数后返回这个对象

    1、创建一个空的对象

    2、链接到原型

    3、绑定this指向,执行构造函数

    4、确保返回的是对象

  2. new共经过了4个阶段

    1、创建一个空对象

    var obj = new Object();
    

    2、设置原型链(当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象)

    obj.__proto__= Func.prototype;
    

    3、让Func中的this指向obj,并执行Func的函数体。(创建新的对象之后,将构造函数的作用域赋给新对象(因此this就指向了这个新对象))

    var result =Func.call(obj);
    

    4、判断Func的返回值类型:

    如果是值类型,返回obj。如果是引用类型,就返回这个引用类型的对象

    if (typeof(result) == "object"){
      func=result;
    }
    else{
        func=obj;;
    }
    

    默认情况下函数返回值为undefined,即没有显示定义返回值的话,但构造函数例外,new构造函数在没有return的情况下默认返回新创建的对象。

    但是,在有显示返回值的情况下,如果返回值为基本数据类型{string,number,null,undefined,Boolean},返回值仍然是新创建的对象。

    只有在显示返回一个非基本数据类型时,函数的返回值才为指定对象。在这种情况下,this所引用的值就会被丢弃了

    参考:blog.csdn.net/lxcao/artic…

  3. 用一个函数实现new 操作符

    function fn (name) {
      this.name = name
    }
    fn.prototype.getName = function () {
      return this.name;
    }
    
    function newFn (fn, name) {
      var obj = Object.create(null);
      obj.__proto__ = fn.prototype;
      var result = fn.call(obj, name);
      if (typeof result == 'object') return result;
      return obj;
    }
    
    var obj = newFn(fn, 'liyuqing')
    console.log(obj)
    //[Object: null prototype] 
    { __proto__: fn { getName: [Function] }, name: 'liyuqing' }
    

call apply &&bind

  1. 三者的区别与联系

    相同点: 都是用来绑定函数运行时的this对象

    不同点: call,apply 是立即执行函数,返回的相应的函数调用结果

    ​ bind的返回值是一个函数

    ​ 两者根据不同的应用场景调用;

  2. call,apply && bind使用

    call:第一个参数为要绑定的对象,若第一个参数不是对象,在非严格模式下,会利用内置构造函数转化为对象:如‘str’ => new String('str),在严格模式下会等于自身;call函数可以有多个参数,除第一个外,剩下的都会传给要执行的函数;

    apply: 第一个参数为要绑定的对象,同call,第二个参数是一个数组,会展开到要执行的函数中;

    bind: 参数同call;

    bind  使用场景
    //html
    <button id="btn"></button>
    //js
    var list = {
        init:function(){
            this.ms = "duyi";
            this.dom = document.getElementById("btn");
            this.bindEvent();
        },
        bindEvent:function(){
            this.dom.onclick = this.showMessage.bind(this);
            //不绑定this的话,回调函数运行时,函数体内的this会指向this.dom,即button dom对象
        },
        showMessage:function(){
            alert(this.ms);
        }
    }
    list.init();
    
  3. call apply && bind 原理实现

    • apply 原理简易实现

      //版本一
      Function.prototype.apply = function(obj,argsArray){
      	if(typeof this !== 'funciton') throw new TypeError('TypeError');
      	if(typeof argsArray === 'undefined' || argsArray === null) argsArray =[];
      	obj.__fn = this;
      	var res = obj.__fn(...argsArray);
      	delete obj.__fn;
      	return res;
      }
      

      以上会出现两个问题:

      1. obj是一个引用传递,若是obj原先就有属性__fn,apply函数中的会替换原先的属性并且最后会删除此属性;

        解决方案1:es6语法,__fn 改为Symbol('__fn'),作为独一无二的值
        解决方案2:利用随机数,模拟生成唯一的值(面试时可用‘__’ + Date.now()时间戳),为了万一重复还是可以再判断原先对象里有无此属性,有的话储藏原始值
        
      2. 扩展运算符是es6语法(想想es5之前是如何实现的?)

        解决方案1: 用eval来执行函数
        解决方案2: 用new Function()  生成执行函数
        
      //完善版本2
      function generateFunctionCode(argsArrayLength){
          var code = 'return arguments[0][arguments[1]](';
          for(var i = 0; i < argsArrayLength; i++){
              if(i > 0){
                  code += ',';
              }
              code += 'arguments[2][' + i + ']';
          }
          code += ')';
          // return arguments[0][arguments[1]](arg1, arg2, arg3...)
          return code;
      }
      Function.prototype.apply = function(obj,argsArray){
      	if(typeof this !== 'funciton') throw new TypeError('TypeError');
      	if(typeof argsArray === 'undefined' || argsArray === null) argsArray =[];
      	//尽量创建唯一的key
      	var fnKey = '__' + Date.now();
      	//储藏可能的原始值
      	var originVal = obj[fnkey];
      	var originTag = obj.hasOwnProperty(fnKey);
      	
      	obj[fnkey] = this;
      	var code = generateFunctionCode(argsArray.length);
      	var res = (new Function(code))(obj,fnkey,argsArray)
      	delete obj.__fn;
      	//恢复之前的值
      	if(originTag){
      		obj[fnkey] = originVal;
      	}
      	return res;
      }
      
    • call 原理实现

      //利用apply  实现call
      Function.prototype.callFn = function call(thisArg){
          var argsArray = [];
          var argumentsLength = arguments.length;
          for(var i = 0; i < argumentsLength - 1; i++){
              argsArray[i] = arguments[i + 1];
          }
          return this.applyFn(thisArg, argsArray);
      }
      
    • bind 原理实现

      //利用call apply  实现bind
       Function.prototype.MyBind = function(target){
           var self = this;
           var args = [].slice.call(arguments,1);
           var temp = function(){};
           var f = function(){
               var _arg = [].slice.call(arguments,0);
               return self.apply(this instanceof temp ? this : ( target || window ),args.concat(_arg) );
           }
           temp.prototype = self.prototype;
           f.prototype = new temp();
           return f;
       }
      

cookie & sessionStorage & localStorage

  1. 三者的相同点

    这是前端本地存储的三种方式,主要用于解决在网页刷新,数据被清空的时候,仍然能够获取之前存储在本地的数据;

  2. 三者的不同点

    • 存储时间

      cookie: 可设置失效时间,没有设置的话,默认时关闭浏览器后失效;

      sessionStorage: 仅在当前网页会话下有效,关闭页面或者关闭浏览器就会立即清除;

      localStorage: 除非手动清除,不然会一直保存;

    • 存放数据大小限制

      cookie:一般是4kb左右;

      sessionStorage & localStorage: 可以存储5MB左右的信息;

    • http请求表现

      cookie:每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题;

      sessionStorage & localStorage: 仅保存在客户端,不参与http通信;

  3. 应用场景

    cookie的使用场景:主要是用来识别用户登录;

    另外cookie每次请求都会携带在http头中,会造成带宽的一个浪费,所以尽可能少用,并且cookie还需要指定作用域,不可以跨域调用,限制比较多,因此在其他能使用storage的情况下,一般都使用storage;

    seesionStorage的使用场景:主要用于页面刷新时,需要保存的临时数据;

    localStorage的使用场景: 主要用于跨页面保存数据;

  4. 使用