# JS

# 原型 / 构造函数 / 实例

  • 原型(prototype): 一个简单的对象,用于实现对象的 属性继承。可以简单的理解成对象的爹。在 Firefox 和 Chrome 中,每个JavaScript对象中都包含一个__proto__ (非标准)的属性指向它爹(该对象的原型),可obj.__proto__进行访问。
  • 构造函数: 可以通过new新建一个对象 的函数。
  • 实例: 通过构造函数和new创建出来的对象,便是实例。 实例通过__proto__指向原型,通过constructor指向构造函数

说了一大堆,大家可能有点懵逼,这里来举个栗子,以Object为例,我们常用的Object便是一个构造函数,因此我们可以通过它构建实例。

// 实例
const instance = new Object()

则此时, 实例为instance, 构造函数为Object,我们知道,构造函数拥有一个prototype的属性指向原型,因此原型为:

// 原型
const prototype = Object.prototype

这里我们可以来看出三者的关系:

实例.__proto__ === 原型

原型.constructor === 构造函数

构造函数.prototype === 原型

// 这条线其实是是基于原型进行获取的,可以理解成一条基于原型的映射线
// 例如: 
// const o = new Object()
// o.constructor === Object   --> true
// o.__proto__ = null;
// o.constructor === Object   --> false
实例.constructor === 构造函数

放大来看,我画了张图供大家彻底理解:

img

# 创建对象

// 第一种方式:字面量
var o1 = {name: 'o1'};
var o2 = new Object({name: 'o2'});
// 第二种方式:构造函数
var M = function (name) { this.name = name; };
var o3 = new M('o3');
// 第三种方式:Object.create 创建的是空对象,只是绑定了原型对象
var p = {name: 'p'};
var o4 = Object.create(p);

# 原型链

原型链是由原型对象组成,每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型,__proto__ 将对象连接起来组成了原型链。是一个用来实现继承和共享属性的有限的对象链。

  • 属性查找机制: 当查找对象的属性时,如果实例对象自身不存在该属性,则沿着原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查找,直至最顶级的原型对象Object.prototype,如还是没找到,则输出undefined
  • 属性修改机制: 只会修改实例对象本身的属性,如果不存在,则进行添加该属性,如果需要修改原型的属性时,则可以用: b.prototype.x = 2;但是这样会造成所有继承于该对象的实例的属性发生改变。

# 继承

在 JS 中,继承通常指的便是 原型链继承,也就是通过指定原型,并可以通过原型链继承原型上的属性或者方法。

  • 最优化: 圣杯模式
var inherit = (function(t,o){
	var F = function(){};
	return function(t,o){
		F.prototype = o.prototype;
		t.prototype = new F();
		t.uber = o.prototype;
		t.prototype.constructor = t;
	}
})();

img

  • 使用 ES6 的语法糖 class / extends

    class Parent {
      constructor(name){
        this.name = name;
        static sayHello(){
            console.log(this.name)
        }
      }
    }
    
    class Child extends Parent {
      constructor(name, age){
        super(name);
        this.age = age;
      }
      sayAge(){
        console.log(this.age)
        return this.age;
      }
    }
    

详解

不要直接说最优解

/**
 * 借助构造函数实现继承
 */
function Parent1 () {
    this.name = 'parent1';
}
Parent1.prototype.say = function () {
};
function Child1 () {
    Parent1.call(this);
    this.type = 'child1';
}
console.log(new Child1(), new Child1().say());
/**
 * 借助原型链实现继承
 */
function Parent2 () {
    this.name = 'parent2';
    this.play = [1, 2, 3];
}
function Child2 () {
    this.type = 'child2';
}
Child2.prototype = new Parent2();
var s1 = new Child2();
var s2 = new Child2();
console.log(s1.play, s2.play);
s1.play.push(4);
/**
 * 组合方式
 */
function Parent3 () {
    this.name = 'parent3';
    this.play = [1, 2, 3];
}
function Child3 () {
    Parent3.call(this);
    this.type = 'child3';
}
Child3.prototype = new Parent3();
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play);
/**
 * 组合继承的优化1
 * @type {String}
 */
function Parent4 () {
    this.name = 'parent4';
    this.play = [1, 2, 3];
}
function Child4 () {
    Parent4.call(this);
    this.type = 'child4';
}
Child4.prototype = Parent4.prototype;
var s5 = new Child4();
var s6 = new Child4();
console.log(s5, s6);
console.log(s5 instanceof Child4, s5 instanceof Parent4);
console.log(s5.constructor);
/**
 * 组合继承的优化2
 */
function Parent5 () {
    this.name = 'parent5';
    this.play = [1, 2, 3];
}
function Child5 () {
    Parent5.call(this);
    this.type = 'child5';
}
Child5.prototype = Object.create(Parent5.prototype);
Child5.prototype.constructor = Child5

# 执行上下文(EC)

简而言之,执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。

# 执行上下文的类型

JavaScript 中有三种执行上下文类型。

  • 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
  • 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
  • Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它。

# 执行栈

执行栈,也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。

当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。

引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。

# 怎么创建执行上下文?

到现在,我们已经看过 JavaScript 怎样管理执行上下文了,现在让我们了解 JavaScript 引擎是怎样创建执行上下文的。

创建执行上下文有两个阶段:1) 创建阶段2) 执行阶段

# The Creation Phase

在 JavaScript 代码执行前,执行上下文将经历创建阶段。在创建阶段会发生三件事:

  1. this 值的决定,即我们所熟知的 This 绑定
  2. 创建词法环境组件。
  3. 创建变量环境组件。

所以执行上下文在概念上表示如下:

ExecutionContext = {
  ThisBinding = <this value>,
  LexicalEnvironment = { ... },
  VariableEnvironment = { ... },
}

# This 绑定:

在全局执行上下文中,this 的值指向全局对象。(在浏览器中,this引用 Window 对象)。

在函数执行上下文中,this 的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined(在严格模式下)

# 词法环境

简单来说词法环境是一种持有标识符—变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。

现在,在词法环境的内部有两个组件:(1) 环境记录器和 (2) 一个外部环境的引用

  1. 环境记录器是存储变量和函数声明的实际位置。
  2. 外部环境的引用意味着它可以访问其父级词法环境(作用域)。

词法环境有两种类型:

  • 全局环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是 null。它拥有内建的 Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有任何用户定义的全局变量,并且 this的值指向全局对象。
  • 函数环境中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。

环境记录器也有两种类型(如上!):

  1. 声明式环境记录器存储变量、函数和参数。
  2. 对象环境记录器用来定义出现在全局上下文中的变量和函数的关系。

简而言之,

  • 全局环境中,环境记录器是对象环境记录器。
  • 函数环境中,环境记录器是声明式环境记录器。

注意 — 对于函数环境声明式环境记录器还包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length

# 变量环境:

它同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。

如上所述,变量环境也是一个词法环境,所以它有着上面定义的词法环境的所有属性。

在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(letconst)绑定,而后者只用来存储 var 变量绑定。

# 执行阶段

这是整篇文章中最简单的部分。在此阶段,完成对所有这些变量的分配,最后执行代码。

注意 — 在执行阶段,如果 JavaScript 引擎不能在源码中声明的实际位置找到 let 变量的值,它会被赋值为 undefined

# 作用域

执行上下文中还包含作用域链。理解作用域之前,先介绍下作用域。作用域其实可理解为该上下文中声明的 变量和声明的作用范围。可分为 块级作用域函数作用域

特性:

  • 声明提前: 一个声明在函数体内都是可见的, 函数优先于变量
  • 非匿名自执行函数,函数变量为 只读 状态,无法修改
let foo = function() { console.log(1) };
(function foo() {
    foo = 10  // 由于foo在函数中只为可读,因此赋值无效
    console.log(foo)
}()) 

// 结果打印:  ƒ foo() { foo = 10 ; console.log(foo) }

# 作用域链

我们知道,我们可以在执行上下文中访问到父级甚至全局的变量,这便是作用域链的功劳。作用域链可以理解为一组对象列表,包含 父级和自身的变量对象,因此我们便能通过作用域链访问到父级里声明的变量或者函数。

  • 由两部分组成:
    • [[scope]]属性: 指向父级变量对象和作用域链,也就是包含了父级的[[scope]]AO
    • AO: 自身活动对象

如此 [[scope]]包含[[scope]],便自上而下形成一条 链式作用域

# 闭包

闭包属于一种特殊的作用域,称为 静态作用域。它的定义可以理解为: 父函数被销毁 的情况下,返回出的子函数的[[scope]]中仍然保留着父级的单变量对象和作用域链,因此可以继续访问到父级的变量对象,这样的函数称为闭包。

  • 闭包会产生一个很经典的问题:
    • 多个子函数的[[scope]]都是同时指向父级,是完全共享的。因此当父级的变量对象被修改时,所有子函数都受到影响。
  • 解决:
    • 变量可以通过 函数参数的形式 传入,避免使用默认的[[scope]]向上查找
    • 使用setTimeout包裹,通过第三个参数传入
    • 使用 块级作用域,让变量成为自己上下文的属性,避免共享

# THIS

# this 永远指向最后调用它的那个对象

例一

    var name = "windowsName";
    function a() {
        var name = "Cherry";

        console.log(this.name);          // windowsName

        console.log("inner:" + this);    // inner: Window
    }
    a();
    console.log("outer:" + this)         // outer: Window

例二

    var name = "windowsName";
    var a = {
        name: "Cherry",
        fn : function () {
            console.log(this.name);      // Cherry
        }
    }
    a.fn();
  

例三

var name = "windowsName";
    var a = {
        name: "Cherry",
        fn : function () {
            console.log(this.name);      // Cherry
        }
    }
    window.a.fn();

例 4:

    var name = "windowsName";
    var a = {
        // name: "Cherry",
        fn : function () {
            console.log(this.name);      // undefined
        }
    }
    window.a.fn();

例 5:

    var name = "windowsName";
    var a = {
        name : null,
        // name: "Cherry",
        fn : function () {
            console.log(this.name);      // windowsName
        }
    }

    var f = a.fn;
    f();

例 6:

    var name = "windowsName";

    function fn() {
        var name = 'Cherry';
        innerFunction();
        function innerFunction() {
            console.log(this.name);      // windowsName
        }
    }

    fn()
    
    是window调的innerFunction

# 怎么改变 this 的指向

改变 this 的指向我总结有以下几种方法:

  • 使用 ES6 的箭头函数

  • 在函数内部使用 _this = this

  • 使用 applycallbind

  • new 实例化一个对象

# 箭头函数

箭头函数的 this 始终指向函数定义时的 this,而非执行时。,箭头函数需要记着这句话:“箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined”。

# apply、call、bind 实现

call、aplly、bind 本质都是改变 this 的指向,不同点 call、aplly 是直接调用函数,bind 是返回一个新的函数。callaplly 就只有参数上不同。

bind实现

  • 箭头函数的 this 永远指向它所在的作用域
  • 函数作为构造函数用 new 关键字调用时,不应该改变其 this 指向,因为 new绑定 的优先级高于 显示绑定硬绑定
Function.prototype.mybind = function(thisArg) {
    if (typeof this !== 'function') {
      throw TypeError("Bind must be called on a function");
    }
    // 拿到参数,为了传给调用者
    const args = Array.prototype.slice.call(arguments, 1),
      // 保存 this
      self = this,
      // 构建一个干净的函数,用于保存原函数的原型
      nop = function() {},
      // 绑定的函数
      bound = function() {
        // this instanceof nop, 判断是否使用 new 来调用 bound
        // 如果是 new 来调用的话,this的指向就是其实例,
        // 如果不是 new 调用的话,就改变 this 指向到指定的对象
        return self.apply(
          this instanceof nop ? this : thisArg,
          args
        );
      };

    // 箭头函数没有 prototype,箭头函数this永远指向它所在的作用域
    if (this.prototype) {
      nop.prototype = this.prototype;
    }
    // 修改绑定函数的原型指向
    bound.prototype = new nop();

    return bound;
  }

测试 mybind

const bar = function() {
  console.log(this.name, arguments);
};

bar.prototype.name = 'bar';

const foo = {
  name: 'foo'
};

const bound = bar.mybind(foo, 22, 33, 44);
new bound(); // bar, [22, 33, 44]
bound(); // foo, [22, 33, 44]

call

bind 是封装了 call 的方法改变了 this 的指向并返回一个新的函数,那么 call 是如何做到改变 this 的指向呢?原理很简单,在方法调用模式下,this 总是指向调用它所在方法的对象,this 的指向与所在方法的调用位置有关,而与方法的声明位置无关(箭头函数特殊)。先写一个小 demo 来理解一下下。

const foo = { name: 'foo' };

foo.fn = function() {
  // 这里的 this 指向了 foo
  // 因为 foo 调用了 fn,
  // fn 的 this 就指向了调用它所在方法的对象 foo 上
  console.log(this.name); // foo
};

利用 this 的机制来实现 call

Function.prototype.mycall = function(thisArg) {
  // this指向调用call的对象
  if (typeof this !== 'function') {
    // 调用call的若不是函数则报错
    throw new TypeError('Error');
  }

  const args = [...arguments].slice(1);
  thisArg = thisArg || window;
  // 将调用call函数的对象添加到thisArg的属性中
  thisArg.fn = this;
  // 执行该属性
  const result = thisArg.fn(...args);
  // 删除该属性
  delete thisArg.fn;
  // 返回函数执行结果
  return result;
};

apply

Function.prototype.myapply = function(thisArg) {
  if (typeof this !== 'function') {
    throw this + ' is not a function';
  }

  const args = arguments[1];

  thisArg.fn = this;

  const result = thisArg.fn(...args);

  delete thisArg.fn;

  return result;
};
# 函数调用

函数调用的方法一共有 4 种

  1. 作为一个函数调用

    fun() 直接调用,不建议在全局中这样做,命名冲突

  2. 函数作为方法调用

    a.fn() 写在对象中 this 永远指向最后调用它的那个对象

  3. 使用构造函数调用函数

    // 构造函数:
    function myFunction(arg1, arg2) {
        this.firstName = arg1;
        this.lastName  = arg2;
    }
    
    // This    creates a new object
    var a = new myFunction("Li","Cherry");
    a.lastName;                             // 返回 "Cherry"
    
    

    new 的过程 这里就简单的来看一下 new 的过程吧: 伪代码表示:

    var a = new myFunction("Li","Cherry");
    
    new myFunction{
        var obj = {};
        obj.__proto__ = myFunction.prototype;
        var result = myFunction.call(obj,"Li","Cherry");
        return typeof result === 'object'? result : obj;
    }
    
    
    1. 创建一个空对象 obj;
    2. 将新创建的空对象的隐式原型指向其构造函数的显示原型。
    3. 使用 call 改变 this 的指向
    4. 如果无返回值或者返回一个非对象值,则将 obj 返回作为新对象;如果返回值是一个新对象的话那么直接直接返回该对象。

    所以我们可以看到,在 new 的过程中,我们是使用 call 改变了 this 的指向。

  4. 作为函数方法调用函数(call、apply)

    匿名函数的 this 永远指向 window

# script 引入方式

  • html 静态<script>引入

  • js 动态插入<script>

    // 加载第三方脚本
    const getScript = (src) => {
      return new Promise((resolve, reject) => {
        let script = document.createElement('script')
        script.src = src
        script.onload = () => {
          resolve({status: 'success'})
        }
        script.onerror = () => {
          reject(new Error('加载脚本失败,请检查链接是否有效'))
        }
        document.head.appendChild(script)
      })
    }
    
  • <script defer>: 延迟加载,元素解析完成后执行

  • <script async>: 异步加载,但执行时会阻塞元素渲染

# defer和async的区别

没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。

defer在DOMContentLoaded 后统一按顺序执行 async 则是一个乱序执行的主,反正对它来说脚本的加载和执行是紧紧挨着的,所以不管你声明的顺序如何,只要它加载完了就会立刻执行,阻塞元素渲染

所以把所有脚本都丢到 之前是最佳实践

# 拷贝

浅拷贝: 以赋值的形式拷贝引用对象,仍指向同一个地址,修改时原对象也会受到影响

  • Object.assign
  • 展开运算符(...)

深拷贝: 完全拷贝一个新对象,修改时原对象不再受到任何影响

  • JSON.parse(JSON.stringify(obj))
    

    最简单

    • 具有循环引用的对象时,报错
    • 当值为函数、undefined、或symbol时,无法拷贝
  • 递归进行逐一赋值

    function deepClone(obj) {
        var result = Array.isArray(obj) ? [] : {};
        for (var key in obj) {
          if (obj.hasOwnProperty(key)) {
            if (typeof obj[key] === 'object' && obj[key]!==null) {
              result[key] = deepClone(obj[key]); 
            } else {
              result[key] = obj[key];
            }
          }
        }
        return result;
      }
    
    function deepClone(arr){
        return JSON.parse(JSON.stringify(arr))
    }
    

开源库

clone

lodash

# new运算符的执行过程

使用new命令时,它后面的函数依次执行下面的步骤。

1、创建一个空对象,作为将要返回的对象实例。 2、将这个空对象的原型,指向构造函数的prototype属性。 3、将这个空对象赋值给函数内部的this关键字。 4、开始执行构造函数内部的代码。

function Person(name,age){
    let obj={};
    obj.__proto__=Person.prototype;
    this=obj;
    this.name=name;
    this.age=age;
    return this;
}

也就是说,构造函数内部,this指的是一个新生成的空对象,所有针对this的操作,都会发生在这个空对象上。构造函数之所以叫“构造函数”,就是说这个函数的目的,就是操作一个空对象(即this对象),将其“构造”为需要的样子。

如果构造函数内部有return语句,而且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象。

# instanceof原理

能在实例的 原型对象链 中找到该构造函数的prototype属性所指向的 原型对象,就返回true。即:

// __proto__: 代表原型对象链
instance.[__proto__...] === instance.constructor.prototype

// return true

# 代码的复用

当你发现任何代码开始写第二遍时,就要开始考虑如何复用。一般有以下的方式:

  • 函数封装
  • 继承
  • 复制extend
  • 混入mixin
  • 借用apply/call

# 类型转换

大家都知道 JS 中在使用运算符号或者对比符时,会自带隐式转换,规则如下:

  • -、*、/、% :一律转换成数值后计算
  • +:
    • 数字 + 字符串 = 字符串, 运算顺序是从左到右
    • 数字 + 对象, 优先调用对象的valueOf -> toString
    • 数字 + boolean/null -> 数字
    • 数字 + undefined -> NaN
  • [1].toString() === '1'
  • {}.toString() === '[object object]'
  • NaN !== NaN 、`+undefined 为 NaN

# 类型判断

判断 Target 的类型,单单用 typeof 并无法完全满足,这其实并不是 bug,本质原因是 JS 的万物皆对象的理论。因此要真正完美判断时,我们需要区分对待:

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

很稳的判断封装:

let class2type = {}
'Array Date RegExp Object Error'.split(' ').forEach(e => class2type[ '[object ' + e + ']' ] = e.toLowerCase()) 

function type(obj) {
    if (obj == null) return String(obj)
    return typeof obj === 'object' ? class2type[ Object.prototype.toString.call(obj) ] || 'object' : typeof obj
}

# 模块化

模块化开发在现代开发中已是必不可少的一部分,它大大提高了项目的可维护、可拓展和可协作性。通常,我们 在浏览器中使用 ES6 的模块化支持,在 Node 中使用 commonjs 的模块化支持。

  • 分类:
    • es6: import / export
    • commonjs: require / module.exports / exports
    • amd: require / defined
  • requireimport的区别
    • require支持 动态导入import不支持,正在提案 (babel 下可支持)
    • require同步 导入,import属于 异步 导入
    • require值拷贝,导出值变化不会影响导入值;import指向 内存地址,导入值会随导出值而变化

# addEventListener

element.addEventListener(event, function, useCapture);

第一个参数是事件的类型 (如 "click" 或 "mousedown").

第二个参数是事件触发后调用的函数。

第三个参数是个布尔值用于描述事件是冒泡还是捕获。该参数是可选的。

注意:不要使用 "on" 前缀。 例如,使用 "click" ,而不是使用 "onclick"。

onclick这种是之前dom1才这样写

事件冒泡或事件捕获?

事件传递有两种方式:冒泡与捕获。

事件传递定义了元素事件触发的顺序。 如果你将

元素插入到

元素中,用户点击

元素, 哪个元素的 "click" 事件先被触发呢?

冒泡 中,内部元素的事件会先被触发,然后再触发外部元素,即:

元素的点击事件先触发,然后会触发

元素的点击事件。

在 *捕获* 中,外部元素的事件会先被触发,然后才会触发内部元素的事件,即:

元素的点击事件先触发 ,然后再触发

元素的点击事件。

addEventListener() 方法可以指定 "useCapture" 参数来设置传递类型:

addEventListener(event, function, useCapture);

默认值false, 即冒泡传递,当值为 true 时, 事件使用捕获传递。

document.getElementById("myDiv").addEventListener("click", myFunction, true);

# 防抖与节流

防抖与节流函数是一种最常用的 高频触发优化方式,能对性能有较大的帮助。

  • 防抖 (debounce): 将多次高频操作优化为只在最后一次执行,通常使用的场景是:用户输入,只需再输入完成后做一次输入校验即可。
function debounce(fn, wait, immediate) {
    let timer = null

    return function() {
        let args = arguments
        let context = this

        if (immediate && !timer) {
            fn.apply(context, args)
        }

        if (timer) clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(context, args)
        }, wait)
    }
}

  • 节流(throttle): 每隔一段时间后执行一次,也就是降低频率,将高频操作优化成低频操作,通常使用场景: 滚动条事件 或者 resize 事件,通常每隔 100~500 ms执行一次即可。
function throttle(fn, wait, immediate) {
    let timer = null
    let callNow = immediate
    
    return function() {
        let context = this,
            args = arguments

        if (callNow) {
            fn.apply(context, args)
            callNow = false
        }

        if (!timer) {
            timer = setTimeout(() => {
                fn.apply(context, args)
                timer = null
            }, wait)
        }
    }
}
# 使用
import {debounce} from '@/assets/js/utils'


methods中  第一种才有用
un: utils.debounce(function () {
  console.log(22222222222)
}, 1000),
scroll () {
  (this.utils.debounce(function () {
    console.log(22222222222)
  }, 1000))()
},

原因:(猜测)
写成函数声明就自调用了,这个函数就等于回调回来的函数,vue initMethods时把回调回来的函数绑定上vue实例

# 函数执行改变this

由于 JS 的设计原理: 在函数中,可以引用运行环境中的变量。因此就需要一个机制来让我们可以在函数体内部获取当前的运行环境,这便是this

因此要明白 this 指向,其实就是要搞清楚 函数的运行环境,说人话就是,谁调用了函数。例如:

  • obj.fn(),便是 obj 调用了函数,既函数中的 this === obj
  • fn(),这里可以看成 window.fn(),因此 this === window

但这种机制并不完全能满足我们的业务需求,因此提供了三种方式可以手动修改 this 的指向:

  • call: fn.call(target, 1, 2)
  • apply: fn.apply(target, [1, 2])
  • bind: fn.bind(target)(1,2)

# ES6/ES7

由于 Babel 的强大和普及,现在 ES6/ES7 基本上已经是现代化开发的必备了。通过新的语法糖,能让代码整体更为简洁和易读。

  • 声明

    • let / const: 块级作用域、不存在变量提升、暂时性死区、不允许重复声明
    • const: 声明常量,无法修改
  • 解构赋值

  • class / extend: 类声明与继承

  • Set / Map: 新的数据结构

  • 异步解决方案:

    • Promise的使用与实现
    • generator:
      • yield: 暂停代码
      • next(): 继续执行代码
    function* helloWorld() {
      yield 'hello';
      yield 'world';
      return 'ending';
    }
    
    const generator = helloWorld();
    
    generator.next()  // { value: 'hello', done: false }
    
    generator.next()  // { value: 'world', done: false }
    
    generator.next()  // { value: 'ending', done: true }
    
    generator.next()  // { value: undefined, done: true }
    
    复制代码
    
    • await / async: 是generator的语法糖, babel中是基于promise实现。
    async function getUserByAsync(){
       let user = await fetchUser();
       return user;
    }
    
    const user = await getUserByAsync()
    console.log(user)
    

# es7 补全位数

item.code.padStart(9, 0)

# async await

async init () {
  this.selectList = await this.loadSelectList()
  this.certificateList = await this.loadIdentityData()
},
  // 证件列表
  return new Promise((resolve, reject) => {
    let data = {
      uid: this.uid,
      method: method
    }
    this.showLoading()
    this.$axios.post('baseApiEntry', data).then(res => {
      this.hideLoading()
      if (res.status === 'success' && res.returndata) {
        resolve(res.returndata.certList)
      } else {
        this.showToast(res.errMsg)
      }
    })
  })
},

# yield

mounted () {
  let a = this.getName(5)
  let c = a.next().value
  let d = a.next(c).value
  let e = a.next(d).value
  console.log(e)
},
methods: {
  * getName (x) {
    let z = yield this.getNum(x)
    let y = yield this.getTotal(z)
    yield this.getTotal(y)
  },
  getNum (x) {
    return x
  },
  getTotal (x) {
    return x + x
  }
}

# promise

  1. 定义

    Promise 是异步编程的一种解决方案,比传统的异步解决方案【回调函数】和【事件】更合理、更强大。现已被 ES6 纳入进规范中。

  2. 痛点解决回调地狱

    回调地狱带来的负面作用有以下几点:

    • 代码臃肿。
    • 可读性差。
    • 耦合度过高,可维护性差。
    • 代码复用性差。
    • 容易滋生 bug。
    • 只能在回调里处理异常。
  3. 业界实现

    业界著名的 Qbluebirdbluebird 甚至号称运行最快的类库。

    现已被 ES6 纳入进规范中

  4. 常规写法

    new Promise(请求1)
        .then(请求2(请求结果1))
        .then(请求3(请求结果2))
        .then(请求4(请求结果3))
        .then(请求5(请求结果4))
        .catch(处理异常(异常信息))
    
     let promise1 = new MyPromise((resolve, reject) => {
            setTimeout(() => {
              resolve(11111)
            }, 2000)
          })
          promise1
            .then(res => {
              console.log(res)
              return new MyPromise((resolve, reject) => {
                this.loadBanner(resolve)
              })
            })
            .then(res => {
              console.log(1111, res)
            })
            .catch(error =>
              console.log(error)
            )
    

img

# Promise.all

Promise.all(promiseList)
  .then((value) => {
    this.getImgAndAnimation(value)
  })
  .catch((e) => {
    console.log(e)
  })
  
promiseList 为promise组成的数组
for循环  所有promise一起调用,在所有都完成或者有一个reject时结束任务

源码

  function promiseAll(promises){
    return new Promise(function(resolve,reject) {
      //promises必须是一个数组
      if(!(promises instanceof Array)) {
        throw new TypeError("promises must be an Array");
      }
      var len = promises.length,
        resolvedCount = 0,
        resolvedArray = new Array(len);
      for(var i = 0;i < len ;i++) {
        (function(i) {
          Promise.resolve(promises[i])
            .then(value => {
              resolvedCount++;
              resolvedArray[i] = value;
              if(resolvedCount == len) {
                return resolve(resolvedArray);
              }
            },re => {
              return reject(re);
            })
            .catch(re => {
              console.log(re);
            })
        })(i)
      }
    })
  }

# AST

抽象语法树 (Abstract Syntax Tree),是将代码逐字母解析成 树状对象 的形式。这是语言之间的转换、代码语法检查,代码风格检查,代码格式化,代码高亮,代码错误提示,代码自动补全等等的基础。例如:

function square(n){
	return n * n
}
复制代码

通过解析转化成的AST如下图:

img

# babel编译原理

  • babylon 将 ES6/ES7 代码解析成 AST
  • babel-traverse 对 AST 进行遍历转译,得到新的 AST
  • 新 AST 通过 babel-generator 转换成 ES5

# shim和polyfill

shim:jquery中的$.ajax就是,判断在什么环境就用对应的api\

polyfill: api统一名字,在没有的环境写个同名的去支持,更优雅

# 函数柯里化

在一个函数中,首先填充几个参数,然后再返回一个新的函数的技术,称为函数的柯里化。通常可用于在不侵入函数的前提下,为函数 预置通用参数,供多次重复调用。

const add = function add(x) {
	return function (y) {
		return x + y
	}
}

const add1 = add(1)

add1(2) === 3
add1(20) === 21

# 循环的区别

Object.keys()
只循环当前对象,不循环其原型链上的
循环可枚举的

for in
循环可枚举的
会循环原型链上的
测试,只向上找一层
通常要带一个hasOwnProperty的判断

Object.getOwnProperty
主要用于返回对象的自有属性,包括可枚举和不可枚举的属性,不包括继承自原型的属性。

# 数组(array)

  • map: 遍历数组,返回回调返回值组成的新数组
  • forEach: 无法break,可以用try/catchthrow new Error来停止
  • filter: 过滤
  • some: 有一项返回true,则整体为true
  • every: 有一项返回false,则整体为false
  • join: 通过指定连接符生成字符串
  • push / pop: 末尾推入和弹出,改变原数组, 返回推入/弹出项
  • unshift / shift: 头部推入和弹出,改变原数组,返回操作项
  • sort(fn) / reverse: 排序与反转,改变原数组
  • concat: 连接数组,不影响原数组, 浅拷贝
  • slice(start, end): 返回截断后的新数组,不改变原数组
  • splice(start, number, value...): 返回删除元素组成的数组,value 为插入项,改变原数组
  • indexOf / lastIndexOf(value, fromIndex): 查找数组项,返回对应的下标
  • reduce / reduceRight(fn(prev, cur), defaultPrev): 两两执行,prev 为上次化简函数的return值,cur 为当前值(从第二项开始)
  • 数组乱序:
// 取巧的一种算法,但是每个位置乱序的概率不同
function mixArr(arr){
    return arr.sort(() => {
        return Math.random() - 0.5;
    })
}

// 著名的Fisher–Yates shuffle 洗牌算法
function shuffle(arr){
    let m = arr.length;
    while(m > 1){
        let index = parseInt(Math.random() * m--);
        [arr[index],arr[m]] = [arr[m],arr[index]];
    }
    return arr;
}
  • 数组 flat: [1,[2,3]] --> [1, 2, 3]
Array.prototype.flat = function() {
    return this.toString().split(',').map(item => +item )
}
// 展平成一级
function flat(arr){
    var result = [];
    for(var i = 0; i < arr.length; i++){
        if(Array.isArray(arr[i])){
            result = result.concat(flat(arr[i]))
        }else{
            result.push(arr[i]);
        }
    }
    return result;
}

//展平成多层
 function flattenByDeep(array,deep){
      var result = [];
      for(var i = 0 ; i < array.length; i++){
          if(Array.isArray(array[i]) && deep >= 1){
                result = result.concat(flattenByDeep(array[i],deep -1))
          }else{
                result.push(array[i])
          }
      }
      return result;
  }

# 数组filter

filter方法经常用,实现起来也比较容易。需要注意的就是filter接收的参数依次为数组当前元素、数组index、整个数组,并返回结果为ture的元素。

Array.prototype.filter = function(fn,context){
    if(typeof fn != 'function'){
        throw new TypeError(`${fn} is not a function`)
    }
    let arr = this;
    let reuslt = []
    for(var i = 0;i < arr.length; i++){
        let temp= fn.call(context,arr[i],i,arr);
        if(temp){
            result.push(arr[i]);
        }
    }
    return result
}

# 数组去重

removeDup (arr) {
  // 数组去重
  let result = []
  let obj = {}
  for (let i = 0; i < arr.length; i++) {
  	let temp = arr[i]
    if (!obj[temp]) {
      result.push(temp)
      obj[temp] = true
    }
  }
  return result
},

Array.from(new Set(arr))

[...new Set(arr)]

# 数组找到没重复的

dedupe (arr1, arr2) {
  // 数组找到没重复的
  return arr1.concat(arr2).filter(function (v, i, arr) {
    return arr.indexOf(v) === arr.lastIndexOf(v)
  })
},

# 数组最大数

let minHour = hourList.reduce(function (a, b) {
          return b < a ? b : a
        })

# 循环

for of
直接将值输出

for in
用于键值对

for
最普通 break打断

forEach
无返回值  抛出错误打断,但不建议

map
有返回值 用于直接改变原数组


# try 打断foreach

try {
  this.category.forEach((item) => {
    if (query === item.cat_name) {
      query = item
      query.is_cate = true
      this.query = query
      this.judgeQuery()
      this.goUrl({
        path: 'SearchResult',
        query: {
          shopId: this.shopId
        }
      })
      throw this.break
    }
  })
} catch (e) {
  return
}

# 迭代

entries
a.next()

# 复制数组

Array.from(arr, function)
可以传个函数对数组操作

Array.of(1,2,3)
创建数组
可以用于复制 Array.of(...arr)

数组也是对象,直接等于是拿的指针

# fill

arr.fill(0,1,2)
填充值 开始位 结束位  结束位不改变

可用于初始数组
Array(6),fill(1)

# copyWithin

arr.copyWithin(0,5)
5位置开始到数组末尾的值按位顶替0到4位的值

arr.copyWithin(0,5,6)
5位置开始到6位的值按位顶替0位的值

# 排序

sort

按字符排序
传个函数 按函数规则排序
arr.sort((a, b) => a - b)  从小到大

let arr = ['a','b','A','B']
arr.sort((a, b) => a.localeCompare(b))
用本地规则排序

reverse

反方向

# includes

arr.includes('a', 1)
值 开始索引      返回true false

# 伪数组转数组

这里把符合以下条件的对象称为伪数组:

1,具有length属性

2,按索引方式(0, 1, 2)存储数据

3,不具有数组的push,pop等方法
Array.prototype.slice.call()
[].slice.call()

更方便的

[...domList]

# Array.from

# 将可枚举的对象或数组、伪数组转为数组并可同时对其中的值进行运算
const someNumbers = {'0': 10, '1': 15, length: 2}
console.log(Array.from(someNumbers, value => value * 2))
function sumArguments() {
  return Array.from(arguments).reduce((sum, num) => sum + num);
}

sumArguments(1, 2, 3); // => 6
# 克隆数组
const numbers = [3, 6, 9];
const numbersCopy = Array.from(numbers);
# 深拷贝
function recursiveClone(val) {
    return Array.isArray(val) ? Array.from(val, recursiveClone) : val;
}

const numbers = [[0, 1, 2], ['one', 'two', 'three']];
const numbersClone = recursiveClone(numbers);

numbersClone; // => [[0, 1, 2], ['one', 'two', 'three']]
numbers[0] === numbersClone[0] // => false

# 用值填充数组
const length = 3
const init = 0
console.log(Array.from({length}, () => init))

用fill也行
console.log(Array(length).fill(init))

必须用参数 直接用数值会报错

当初始化对象时,用fill出来的会是同一个对象,array.from的mapFunction每次都是新生成一个对象

用Array(length).map(() => init)

不能出现[0,0,0] 因为map会自动过滤掉空的(也称为稀疏数组)

# 根据索引生成数组
console.log(Array.from({ length: end }, (_, index) => index))

end为终止值   (_, index) index索引  相当于map
# 数组去重
unique (arr) {
  return Array.from(new Set(arr))
}

# Array.of

Array.of(1, '2') => [1, '2']

# copyWithin

// 将3号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)
// [4, 2, 3, 4, 5]

// -2相当于3号位,-1相当于4号位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1)
// [4, 2, 3, 4, 5]

// 将3号位复制到0号位
[].copyWithin.call({length: 5, 3: 1}, 0, 3)
// {0: 1, 3: 1, length: 5}

// 将2号位到数组结束,复制到0号位
let i32a = new Int32Array([1, 2, 3, 4, 5]);
i32a.copyWithin(0, 2);
// Int32Array [3, 4, 5, 4, 5]

// 对于没有部署 TypedArray 的 copyWithin 方法的平台
// 需要采用下面的写法
[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);
// Int32Array [4, 2, 3, 4, 5]

# 数组拼接、降维

Array.prototype.concat.apply([], [1, [2]])

# Object

# Object.freeze(obj)

冻结对象,在严格模式下不能修改obj

完全冻结

var constantize = (obj) => {
  Object.freeze(obj);
  Object.keys(obj).forEach( (key, i) => {
    if ( typeof obj[key] === 'object' ) {
      constantize( obj[key] );
    }
  });
};

# 判断对象是不是空

Object.keys(obj).length

# 对象是否含有该键值

return element in items
return Object.prototype.hasOwnProperty.call(this.items, element)

# Object.entries()

for (let [key, value] of Object.entries(object1)) {
  console.log(`${key}: ${value}`);
}

# dom操作

# getElementById、querySelector方法的区别

getElementByXXX 获取的是动态集合,querySelecto或querySelectorAll获取的是静态集合。

<ul id="ul">
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ul>

//demo1
var ul = document.getElementById('ul'),
     lis = document.getElementsByTagName("li");
for(var i = 0; i < lis.length ; i++){
     ul.appendChild(document.createElement("li"));
}
console.log(lis.length);  //会卡死浏览器,因为li的长度一直在变大

//demo2
var ul = document.getElementById('ul'),
    lis = document.querySelectorAll("li");
for(var i = 0; i < lis.length ; i++){
     ul.appendChild(document.createElement("li"));
}
console.log(lis.length);  //3

# 获取样式

document.documentElement.style.xx

# dom操作

e.currentTarget
target.parentElement.nextElementSibling.style.display

# 事件源

srcElement 在 ie下起作用

e.target || e.srcElement

# 设计模式

# 工厂模式

工厂模式的目的,就是为了实现无脑传参

将创建对象的过程单独封装

抽离公共部分

function User(name , age, career, work) {
    this.name = name
    this.age = age
    this.career = career 
    this.work = work
}

function Factory(name, age, career) {
    let work
    switch(career) {
        case 'coder':
            work =  ['写代码','写系分', '修Bug'] 
            break
        case 'product manager':
            work = ['订会议室', '写PRD', '催更']
            break
        case 'boss':
            work = ['喝茶', '看报', '见客户']
        case 'xxx':
            // 其它工种的职责分配
            ...
            
    return new User(name, age, career, work)
}

# 抽象工厂

# 单例模式

# 原型模式

# 装饰器模式

# 适配器模式

# 代理模式

# 策略模式

# 状态模式

# 观察者模式

# 迭代器模式

# 碎片知识

# console打印样式

const style = 'color: red; background: skyblue; font-size: 24px; padding: 10px; font-weight: bold;'
console.log('%c都看到这里了,还不点个赞?', style)

注意%c

# 监听复制

window.addEventListener('paste', (e) => {
  // Prevent the default pasting event and stop bubbling
  e.preventDefault()
  e.stopPropagation()
  // Get the clipboard data
  let paste = (e.clipboardData || window.clipboardData).getData('text/html')
// Do something with paste like remove non-UTF-8 characters
  paste = paste.replace(/style/gi, 'data-style')
  console.log(paste)
// Find the cursor location or highlighted area
  const selection = window.getSelection()
// Cancel the paste operation if the cursor or highlighted area isn't found
  if (!selection.rangeCount) return false
  var div = document.createElement('div')
  div.innerHTML = paste
// Paste the modified clipboard content where it was intended to go
  selection.getRangeAt(0).insertNode(div)
})

# 微信公众号图片防盗链

// 不提供来源页面,以便能加载设置防盗链网站的图片
<meta name="referrer" content="never">

动态添加

const ohead = document.getElementsByTagName('head')[0]
const meta = Array.from(ohead.getElementsByTagName('meta'))
const index = meta.findIndex(item => {
  return item.name === 'referrer'
})
if (index === -1) {
  let node = document.createElement('meta')
  node.name = 'referrer'
  node.content = 'never'
  document.head.appendChild(node)
}

# placeholder chrome不消失

 onfocus="this.placeholder=''"
 onblur="this.placeholder='请输入'"
 
 vue中用ref可改this.$refs.xxx.placeholder = ''

# swiper tab改成等距

slidesPerView: 'auto',

padding: 0 0.2rem;

.swiper-slide {
	width: auto;
}
<swiper class="match-list" :options="pageOption" ref="pageSwiper">
  <swiper-slide class="match-item" v-for="(item, index) in scheduleList" :key="index">
    <div>
    </div>
  </swiper-slide>
</swiper>
pageOption: {
  autoplay: false,
  initialSlide: 0,
  slidesPerView: 2.2
  // on: {
  //   slideChange () {
  //     self.curNav = this.activeIndex
  //   }
  // }
}

# 纪录页面滚动位置

重复使用同一个组件时

activated () {
      let scrollTop = sessionStorage.getItem('matchTop') ? JSON.parse(sessionStorage.getItem('matchTop'))[this.index] : []
      console.log(scrollTop)
      if (scrollTop && +scrollTop > 0) {
        this.$refs.content.scrollTop = scrollTop
      }
    },
    deactivated () {
      let topList = sessionStorage.getItem('matchTop') ? JSON.parse(sessionStorage.getItem('matchTop')) : []
      topList[this.index] = this.scrollTop
      sessionStorage.setItem('matchTop', JSON.stringify(topList))
    },
mounted () {
  // 载入数据
  let topList = []
  sessionStorage.setItem('scrollTop', JSON.stringify(topList))
},
contentScroll (e) {
  let target = e.target || e.srcElement
  this.scrollTop = target.scrollTop
  if (target.scrollTop + target.clientHeight >= target.scrollHeight - 5) {
    this.loadOrderList()
  }
}

@scroll="contentScroll"

contentScroll (e) {
  let target = e.target || e.srcElement
  this.scrollTop = target.scrollTop
  if (target.scrollTop + target.clientHeight >= target.scrollHeight - 3) {
    this.loadArticlesByTypeAndId()
  }
},
activated () {
  this.scrollTop = sessionStorage.getItem('scrollTop')
  this.$refs.list.scrollTo(0, this.scrollTop)
},
deactivated () {
  sessionStorage.setItem('scrollTop', this.scrollTop)
},
import utils from '@/assets/js/utils.js'
      contentScrollDebounce: utils.debounce(function (e) {
        this.contentScroll(e)
      }, 100),

# setTimeout与setInterval

在需要重复发送请求或者某些效果的时候,一般都会想到使用setInterval,但是它的一些弊端,会给程序带来很大的隐患

一、弊端 1.setInterval对自己调用的代码是否报错漠不关心。即使调用的代码报错了,它依然会持续的调用下去 2.setInterval无视网络延迟。在使用ajax轮询服务器是否有新数据时,必定会有一些人会使用setInterval,然而无论网络状况如何,它都会去一遍又一遍的发送请求,如果网络状况不良,一个请求发出,还没有返回结果,它会坚持不懈的继续发送请求,最后导致的结果就是请求堆积。 3.setInterval并不定时。如果它调用的代码执行的时间小于定时的时间,它会跳过调用,这就导致无法按照你需要的执行次数或无法得到你想要的结果。

二、解决方案 使用setTimeout代替setInterval。 可以给setTimeout设置时间后,在最后调用自身。如果希望“匀速”触发。可以计算代码执行时间,用希望的延迟减去上次执行的时间。

注:有一种想法是将setInterval的延迟时间设置的长于上述的几种时间,来达到绝对的均速调用。但事实上,js的计时器因为自身机制的原因,存在4ms–15ms的误差

对于第二个参数有以下需要注意的地方:

当第二个参数缺省时,默认为 0;

当指定的值小于 4 毫秒,则增加到 4ms(4ms 是 HTML5 标准指定的,对于 2010 年及之前的浏览器则是 10ms);也就是说至少需要4毫秒,该setTimeout()拿到任务队列中。

# textarea换行

textareaText (val) {
  if (val) {
    let temp = val
    temp = temp.replace(/\n/g, '<br/>')
    this.$refs.competitionName.innerHTML = temp
  } else {
    this.$refs.competitionName.innerHTML = '请填写赛事名称'
  }
},

# 弧形文字

CircleType.js

arctext不好使

# input上传图片

FileReader

changePic () {
  if (!document.getElementById('file').files[0]) {
    return
  }
  let reads = new FileReader()
  let _this = this
  let f = document.getElementById('file').files[0]
  reads.readAsDataURL(f)
  reads.onload = function (e) {
    _this.$refs.inputImg.src = this.result
    _this.imgSrc = this.result
  }
},

base64格式,可直接显示在img标签里

@change方式

fileChange (e) {
  let files = e.target.files || e.srcElement.files
  for (let i = 0; i < files.length && i < 9; i++) {
    let size = files[i].size
    if (size > 1024 * 1024 * 100) {
      this.showToast('文件过大,无法上传')
      return
    }
  }
},

获得的是blob格式,上传到服务器成图片格式

multiple 支持多选  安卓下只有qq浏览器可以

# input上传图片转base64

fileChange (e) {
  const file = e.target.files[0]
  const reader = new FileReader()
  reader.onloadend = url => {
    this.chooseImg = reader.result
    this.init()
  }
  reader.readAsDataURL(file)
}

# 保存base64图片

用canvas导出 再用a标签下载

saveImg () {
  const url = this.$refs.canvas.toDataURL('image/png')
  let a = document.createElement('a')
  a.href = url
  a.setAttribute('download', 'chart-download')
  a.click()
  a = null
}

# 分页加载逻辑

1.没有更多return掉

2.数据concat

3.获取的数据===10 page++ 不然 nomore=true

4.拼接的数据===0 说明没有 empty true

# 列表页模板

const list = data.data || []
this.goodsList = this.goodsList.concat(list)
if (list.length === LIMIT) {
  this.page++
} else {
  this.noMore = true
}
this.isEmpty = this.goodsList.length === 0
page: 1,
noMore: false,
isEmpty: false
<!-- 裁判客户列表 -->
<!-- Create by luozheng on 2019/08/15 -->
<style lang="less" scoped>
  @mainColor: #3db657;
  .container {
    width: 100%;
    height: 100%;
    background-color: #f6f6f6;
    .content-empty {
      width: 100%;
      height: 100%;
      text-align: center;
      img {
        width: 1.18rem;
        height: 1.32rem;
      }
      .empty-text {
        font-size: 0.34rem;
        color: #9ea0a2;
        padding-top: 0.22rem;
        padding-bottom: 2rem;
      }
    }
  }
  .mint-loadmore {
    min-height: 10rem;
    .mint-loadmore-content {
      min-height: 100%;
    }
  }
</style>

<template>
  <div class="container overscroll" @scroll="contentScroll">
    <div v-if="!empty" style="min-height: 100%">
      <mt-loadmore ref="refreshData" :top-method="refreshData">
        <referee-custom-item
          v-for="item in customList"
          v-if="+item.targetType === 4"
          :item="item"
          :index="index"
          :key="item.targetName"
          style="margin-top: 0.32rem;"
          @clickItem="clickItem"
        ></referee-custom-item>
        <base-line v-if="noMore"></base-line>
      </mt-loadmore>
    </div>
    <div v-else class="content-empty _flex">
      <div>
        <img src="http://image.greenplayer.cn/share/img/icon/mark_brand_goods_empty.png" alt="">
        <div class="empty-text">暂无纪录</div>
      </div>
    </div>
  </div>
</template>

<script>
  const RefereeCustomItem = () => import('@/components/referee/RefereeCustomItem')
  const BaseLine = () => import('@/components/base/BaseLine')
  const LIMIT = 10

  export default {
    name: 'RefereeCustomList',
    components: {RefereeCustomItem, BaseLine},
    data () {
      return {
        refereeId: this.getQueryString('refereeId'),
        customList: [],
        page: 1,
        noMore: false,
        empty: false,
        old: 0,
        now: 0
      }
    },
    mounted () {
      document.title = '执法客户'
      this.loadCustomList()
    },
    methods: {
      refreshData () {
        this.page = 1
        this.noMore = false
        this.empty = false
        this.customList = []
        this.$refs.refreshData.onTopLoaded()
        this.loadCustomList()
      },
      contentScroll (e) {
        let target = e.target || e.srcElement
        this.scrollTop = target.scrollTop
        if (target.scrollTop + target.clientHeight >= target.scrollHeight - 3) {
          this.loadCustomList()
        }
      },
      clickItem (item, index) {
        this.goUrl({
          path: 'RefereeServeList',
          query: {
            targetId: item.targetId,
            refereeId: this.refereeId,
            targetType: item.targetType
          }
        })
      },
      loadCustomList () {
        this.now = (new Date()).getTime()
        if (this.now - this.old < 300) {    // 防抖
          return
        }
        this.old = this.now
        if (this.noMore) {
          return
        }
        let data = {
          method: 'user_referee_loadCustomerList',
          refereeId: this.refereeId,
          page: this.page,
          limit: LIMIT
        }
        this.showLoading()
        this.$axios.post('baseApiEntry', data).then(res => {
          if (res.status === 'success' && res.returndata) {
            this.customList = this.customList.concat(res.returndata)
            if (res.returndata.length === LIMIT) {
              this.page++
            } else {
              this.noMore = true
            }
            if (this.customList.length === 0) {
              this.isEmpty = true
            }
          } else {
            this.showToast(res.errMsg)
          }
        }).finally(() => {
          this.hideLoading()
        })
      }
    }
  }
</script>

# 跳锚点

加个id,名字自己取
:id="'product' + index"
// 跳转到对应锚点
document.getElementById('product' + index).scrollIntoView(true)

scrollIntoView(true) 跳到元素顶部
scrollIntoView(flase) 跳到元素底部

# 调相机

<input type="file" accept="image/*" capture="camera">  
<input type="file" accept="video/*" capture="camcorder">  
<input type="file" accept="audio/*" capture="microphone">  

camera:照相机;camcorder:摄像机;microphone:录音机。

multiple测试加不加都没作用

capture加了直接调对应的

# DomContentLoaded与load区别

https://www.cnblogs.com/caizhenbo/p/6679478.html

# rem方案

<!-- 计算rem 设计稿为750,设置rootElement fontSize在设备宽为750时 = 100px -->
<script>
  (function (e, d) {
    var a = 750, f = e.documentElement, c = 'orientationchange' in window ? 'orientationchange' : 'resize',
      b = function () {
        var g = f.clientWidth
        if (!g) {
          return
        }
        if (g >= a) {
          f.style.fontSize = '100px'
        } else {
          f.style.fontSize = 100 * (g / a) + 'px'
        }
      }
    if (!e.addEventListener) {
      return
    }
    d.addEventListener(c, b, false)
    e.addEventListener('DOMContentLoaded', b, false)
  })(document, window)
</script>

在domcontentloaded和页面大小改变时触发改变根元素字体大小

限制了页面最大750px 所以这里在大于等于750时直接100px

加上dpr

  /* 设计稿是750,采用1:100的比例,font-size为100 * (docEl.clientWidth * dpr / 750) */
  var dpr, rem, scale
  var docEl = document.documentElement
  var fontEl = document.createElement('style')
  var metaEl = document.querySelector('meta[name="viewport"]')
  dpr = window.devicePixelRatio || 1
  rem = 100 * (docEl.clientWidth * dpr / 750)
  console.log(rem, docEl.clientWidth, dpr)
  scale = 1 / dpr
  // 设置viewport,进行缩放,达到高清效果
  metaEl.setAttribute('content', 'width=' + dpr * docEl.clientWidth + ',initial-scale=' + scale + ',maximum-scale=' + scale + ', minimum-scale=' + scale + ',user-scalable=no')
  // 设置data-dpr属性,留作的css hack之用,解决图片模糊问题和1px细线问题
  docEl.setAttribute('data-dpr', dpr)
  // 动态写入样式
  docEl.firstElementChild.appendChild(fontEl)
  docEl.style.fontSize = rem + 'px'

# 手动实现懒加载

# getBoundingClientRect
this.$refs.container.addEventListener('scroll', this.load)
load () {
  // 遍历所有节点调用函数判断是否加载图片
  this.imgArray.forEach(item => this.getBound(item) && this.loadImg(item))
},
getBound (el) {
  // 判断图片是否出现在可视窗口中
  // getBoundingClientRect貌似有兼容问题,可改为offsetTop-页面滚动高度
  // (el.offsetTop - this.$refs.container.scrollTop)
  let bound = el.getBoundingClientRect()
  let clientHeight = window.innerHeight
  // 图片距离顶部的距离 <= 浏览器可视化的高度,从而推算出是否需要加载
  return bound.top <= clientHeight - 100 // -100是为了看到效果,您也可以去掉
},
loadImg (el) {
  // 将自定义属性赋值给标签src属性
  el.src = el.getAttribute('data-src')
},
handleImage () {
  // 处理文章内图片大小,因为抓取的新闻图片大小显示不可控
  this.imgArray = document.querySelectorAll('.detail-container img')
  this.imgArray.forEach((item) => {
    item.setAttribute('data-src', item.src)
    item.setAttribute('src', 'data:image/svg+xml;base64,PHN2ZyBpZD0i5Zu+5bGCXzEiIGRhdGEtbmFtZT0i5Zu+5bGCIDEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUwMCA1MDAiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDojZmZmO30uY2xzLTJ7ZmlsbDojZWVlO308L3N0eWxlPjwvZGVmcz48dGl0bGU+5Zu+54mH5Y2g5L2Nb3LliqDovb08L3RpdGxlPjxyZWN0IGNsYXNzPSJjbHMtMSIgd2lkdGg9IjUwMCIgaGVpZ2h0PSI1MDAiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yMjUuNjQsMTUyLjg3aDM4LjlMMjI5LDE0MS44MlptMCwwIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMjEyLjg3LDExMy44OGwtMTEuODMsMzloMTEuMThsNy43My0yNS40OCwxNjMuNDksNTAuODRMMzI3LDM2NC40NWwtMjEuMzEtNi42M3YxMS4zMkwzMzQsMzc4LDM5Ni44LDE3MS4wN1ptMCwwIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMzA1LjY0LDE2NS42NXYxNTRsMTkuNjQsNi4xMUwzNjgsMTg1Wk0xMjYuNzEsMTgzLjU5SDI3Mi4wOVYzMzAuNzhIMTI2LjcxWm0wLDAiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xMDMuMiwzNzguMTJIMjk1LjYxVjE2MS42OEgxMDMuMlpNMTEzLjg5LDE3Mi41aDE3MVYzNjcuM2gtMTcxWm0wLDAiLz48L3N2Zz4=')
  })
},
# IntersectionObserver
let img = document.getElementsByTagName("img");

const observer = new IntersectionObserver(changes => {
  //changes 是被观察的元素集合
  for(let i = 0, len = changes.length; i < len; i++) {
    let change = changes[i];
    // 通过这个属性判断是否在视口中
    if(change.isIntersecting) {
      const imgElement = change.target;
      imgElement.src = imgElement.getAttribute("data-src");
      observer.unobserve(imgElement);
    }
  }
})
Array.from(img).forEach(item => observer.observe(item));

# 改写console

console.log = (param) => {
  var element = document.createElement('div')
  element.style.background = 'green'
  element.style.fontSize = '20px'
  switch (typeof param) {
    case 'string' :
      element.innerHTML = param
      break
    case 'object':
      element.innerHTML = JSON.stringify(param)
      break
  }
  this.$refs.container.appendChild(element)
}
console.log('fwefwef')
console.log({name: 19})

# window.location

属性 描述
hash 从井号 (#) 开始的 URL(锚)
host 主机名和当前 URL 的端口号
hostname 当前 URL 的主机名
href 完整的 URL
pathname 当前 URL 的路径部分
port 当前 URL 的端口号
protocol 当前 URL 的协议
search 从问号 (?) 开始的 URL(查询部分)

# import和require区别

都是被模块化使用

  1. a. require是CommonJs的语法(AMD规范引入方式),CommonJs的模块是对象。

    b. import是es6的一个语法标准(浏览器不支持,本质是使用node中的babel将es6转码为es5再执行,import会被转码为 require),es6模块不是对象

  2. a. require是运行时加载整个模块(即模块中所有方法),生成一个对象,再从对象上读取它的方法(只有运行时才能得到这 个对象,不能在编译时做到静态化),理论上可以用在代码的任何地方

    b. import是编译时调用,确定模块的依赖关系,输入变量(es6模块不是对象,而是通过export命令指定输出代码,再通过 import输入,只加载import中导的方法,其他方法不加载),import具有提升效果,会提升到模块的头部(编译时执行)

     export和import可以位于模块中的任何位置,但是必须是在模块顶层,如果在其他作用域内,会报错
    
     es6这样的设计可以提高编译器效率,但没法实现运行时加载
    
  3. a. require是赋值过程,把require的结果(对象,数字,函数等),默认是export的一个对象,赋给某个变量(复制或浅拷贝)

    b. import是解构过程(需要谁,加载谁)

# Canvas和SVG图形的区别

# canvas

依赖分辨率

不支持事件处理

能以png、jpg格式保存图像

适合图像密集型的游戏,其中的许多对象会被频繁重绘

# svg

不依赖分辨率 矢量的

支持事件处理器 xml编写

适合带有大型渲染区域的应用程序 谷歌地图

复杂度高会减慢渲染速度(任何过度使用DOM的应用都不快)

不适合游戏

# haslayout

ie7 用来判断该元素有没有自己的布局

element.currentStyle.hasLayout
true  false

# 保护js的方式

  1. 压缩

    替换变量名 换成abc这种 主要是压缩体积

  2. 混淆

    增加无用代码,删除注释缩进 降低代码的可读性

  3. 加密

    escape unescape

# 替换页面

location.replace(`${location.protocol}//${location.host}${location.pathname}#/InsuranceMall?targetId=${this.targetId}&targetType=${this.targetType}${this.isCustom ? '&isCustom=1' : ''}`)

# 多条件判断简写

像这种

+this.modelType === 13 || +this.modelType === 14

可以写成

[13, 14].includes(+this.modelType)

# 对象代替switch

对象用键值 找的时候obj[key] 比switch方便一些

# 图片加载完成事件

/**
 * 图片加载完成事件
 * @param img
 * @param resolve
 */
imgLoadEvent (img, resolve) {
  let imgEle = document.createElement('img')
  imgEle.src = img
  imgEle.onload = () => {
    resolve(img)
  }
},

直接调用promise的resolve

# 调摄像头并视频显示出来

startMedia () {
  let video = document.getElementById('video')
  // let canvas = document.getElementById('canvas')
  if (navigator.mediaDevices.getUserMedia || navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia) {
  // 调用用户媒体设备, 访问摄像头
    this.getUserMedia({
      video: {
        width: 480,
        height: 320
      }
    }, stream => {
      let compatibleUrl = window.URL || window.webkitURL
      console.log(111111, stream)
      console.log(compatibleUrl)
      video.srcObject = stream
      video.play()
    }, error => {
      console.log(`访问用户媒体设备失败${error.name}, ${error.message}`)
    })
  } else {
    alert('不支持访问用户媒体')
  }
},
getUserMedia (constraints, success, error) {
  if (navigator.mediaDevices.getUserMedia) {
    // 最新的标准API
    navigator.mediaDevices.getUserMedia(constraints).then(success).catch(error)
  } else if (navigator.webkitGetUserMedia) {
    // webkit核心浏览器
    navigator.webkitGetUserMedia(constraints, success, error)
  } else if (navigator.mozGetUserMedia) {
    // firfox浏览器
    navigator.mozGetUserMedia(constraints, success, error)
  } else if (navigator.getUserMedia) {
    // 旧版API
    navigator.getUserMedia(constraints, success, error)
  }
},

# 让点击弹窗穿透

point-events:none;

# 获取地理位置

navigator.geolocation.getCurrentPosition

# 并行和并发、同步和异步、阻塞和非阻塞之间有什么区别?

并发是一次处理很多事情。 并行是同时做很多事情。 同步轮询查询结果。 异步等有结果后会通知进而执行下一步。 阻塞等待结果的时候不能去处理别的。 非阻塞等待结果的时候能去处理别的。

# 大数据渲染

# requestAnimationFrame

由于屏幕种类,分辨率,屏幕尺寸的不同,屏幕自动刷新的频率不同,使用requestAnimationFrame可以自动适配屏幕刷新频率。避免丢帧。

与setTimeout相比,requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机。除此以外,还可以节省CPU,函数节流。

# 时间分片
function init () {
    let ul = document.getElementById('container')
    let total = 100000
    let once = 20
    // let page = total / once
    let index = 0

    function loop (curTotal, curIndex) {
      if (curTotal <= 0) {
        return false
      }
      let pageCount = Math.min(curTotal, once)
      window.requestAnimationFrame(function () {
        let fragment = document.createDocumentFragment()
        for (let i = 0; i < pageCount; i++) {
          let li = document.createElement('li')
          li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
          fragment.appendChild(li)
        }
        ul.appendChild(fragment)
        loop(curTotal - pageCount, curIndex + pageCount)
      })
      // setTimeout(() => {
      //   for (let i = 0; i < pageCount; i++) {
      //     let li = document.createElement('li')
      //     li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
      //     ul.appendChild(li)
      //   }
      //   loop(curTotal - pageCount, curIndex + pageCount)
      // }, 0)
    }
    loop(total, index)
  }
# 虚拟列表简易版
<!-- 物流详情页 -->
<!-- Create by luozheng on 2019/08/22 -->
<style lang="less" scoped>
  @mainColor: #3db657;
  .container {
    width: 100%;
    height: 100%;
  }
  .list-view {
    height: 400px;
    overflow: auto;
    position: relative;
    border: 1px solid #aaa;
  }
  .list-view-phantom {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    z-index: -1;
  }
  .list-view-content {
    left: 0;
    right: 0;
    top: 0;
    position: absolute;
  }
  .list-view-item {
    padding: 5px;
    color: #666;
    line-height: 30px;
    box-sizing: border-box;
  }
</style>

<template>
  <div
    class="list-view"
    @scroll="handleScroll">
    <div
      class="list-view-phantom"
      :style="{height: contentHeight}"
    >
    </div>
    <div
      ref="content"
      class="list-view-content"
    >
      <component
        :is="comp"
        class="list-view-item"
        :style="{ height: itemHeight + 'px'}"
        v-for="(item, index) in visibleData"
        :key="comp + index"
        :content="item"
      >
      </component>
    </div>
  </div>

</template>

<script>
  const GoodsItem = () => import('@/components/base/ImgTitleArrowBox')
  export default {
    name: 'ListView',
    components: {GoodsItem},
    data () {
      return {
        visibleData: []
      }
    },
    props: {
      data: {
        type: Array,
        required: true
      },
      itemHeight: {
        type: Number,
        default: 30
      },
      comp: {
        default: ''
      }
    },
    computed: {
      contentHeight () {
        return this.data.length * this.itemHeight + 'px'
      }
    },
    mounted () {
      this.updateVisibleData()
    },
    methods: {
      updateVisibleData (scrollTop) {
        scrollTop = scrollTop || 0
        const visibleCount = Math.ceil(this.$el.clientHeight / this.itemHeight) // 取得可见区域的可见列表项数量
        const start = Math.floor(scrollTop / this.itemHeight) // 取得可见区域的起始数据索引
        const end = start + visibleCount // 取得可见区域的结束数据索引
        this.visibleData = this.data.slice(start, end) // 计算出可见区域对应的数据,让 Vue.js 更新
        if (end === this.data.length) {
          this.$emit('onBottom')
        }
        this.$refs.content.style.webkitTransform = `translate3d(0, ${start * this.itemHeight}px, 0)` // 把可见区域的 top 设置为起始元素在整个列表中的位置(使用 transform 是为了更好的性能)
      },
      handleScroll () {
        const scrollTop = this.$el.scrollTop
        this.updateVisibleData(scrollTop)
      }
    }
  }
</script>

# js为什么是单线程的

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

详细 http://www.ruanyifeng.com/blog/2014/10/event-loop.html

# js库习惯在代码开头处添加分号有什么作用呢

;相当于上一个语句的结束,可以防止在js打包的时候,某一个js文件末尾未加;导致两个js文件被判定为一条语句。 也可以替换为void

# attribute和property的区别

  • attribute
    • 元素在HTML中的键值对
    • attribute 会始终保持 html 代码中的初始值(除了href)
  • property
    • attribute在对应的JS DOM节点上的对象属性
    • Property是有可能变化的(跟随用户操作).
  • 其他补充
    • 有一些特殊的 attribute, 它们对应的 Property 名称会发生改变
      • for (attr) => htmlFor (prop)
      • class (attr) => className (prop)
    • 以下图来自@justjavac (opens new window) (https://zhuanlan.zhihu.com/p/70671215) image (opens new window)

# BLOB URL

blob是一种二进制文件格式,不可变,最原始数据的类文件对象 好处就是作为多种文件类型格式的中间态进行传输和转换 我的理解是,二进制文件方便用流分段处理

# 获取当前域名

document.domain;
window.location.host;

https://www.cnblogs.com/wangdahai/p/6221399.html

# 如何解决跨域?

1.主域相同,子域不同,可以设置在两个页面都设置document.domain = ‘xxx.com’然后,两个文档就可以进行交互。

https://blog.csdn.net/nlznlz/article/details/79506655

2.主域和子域都不同,则可以使用CDM(cross document messaging)进行跨域消息的传递。 发送消息: 使用postmessage方法 接受消息: 监听message事件

/**
 * 跨域通信的几种方法
 */
// jsonp工作原理,参考jsonp.js


// 利用hash,场景是当前页面 A 通过iframe或frame嵌入了跨域的页面 B
// 在A中伪代码如下:
var B = document.getElementsByTagName('iframe');
B.src = B.src + '#' + 'data';
// 在B中的伪代码如下
window.onhashchange = function () {
    var data = window.location.hash;
};


// postMessage
// 窗口A(http:A.com)向跨域的窗口B(http:B.com)发送信息
Bwindow.postMessage('data', 'http://B.com');
// 在窗口B中监听
Awindow.addEventListener('message', function (event) {
    console.log(event.origin);
    console.log(event.source);
    console.log(event.data);
}, false);


// Websocket【参考资料】http://www.ruanyifeng.com/blog/2017/05/websocket.html
var ws = new WebSocket('wss://echo.websocket.org');
ws.onopen = function (evt) {
    console.log('Connection open ...');
    ws.send('Hello WebSockets!');
};
ws.onmessage = function (evt) {
    console.log('Received Message: ', evt.data);
    ws.close();
};
ws.onclose = function (evt) {
    console.log('Connection closed.');
};

// CORS【参考资料】http://www.ruanyifeng.com/blog/2016/04/cors.html
// url(必选),options(可选)
fetch('/some/url/', {
    method: 'get',
}).then(function (response) {
}).catch(function (err) {
  // 出错了,等价于 then 的第二个参数,但这样更好用更直观
});

# 手写getElementsByClassName

单个版

getElementsByClassName () {
        const query = Array.from(arguments)
        if (query.length === 0) return
        try {
          let tag = document.getElementsByTagName('*') || document.all
          let array = []
          for (let i = 0; i < tag.length; i++) {
            if (tag[i].className.indexOf(query[0]) !== -1) {
              array.push(tag[i])
            }
          }
          return array
        } catch (e) {
        }
      },

# location

location.origin

"https://share.greenplayer.cn"
location.host

"share.greenplayer.cn"

# atob和btoa的用法

是window的两个对象,btoa:binary to ascii;(base64的编码) atob:ascii to binary;(base64的解码)无法用于Unicode字符

// Define the string
var string = 'Hello World!';

// Encode the String
var encodedString = btoa(string);
console.log(encodedString); // Outputs: "SGVsbG8gV29ybGQh"

// Decode the String
var decodedString = atob(encodedString);
console.log(decodedString); // Outputs: "Hello World!"

# video标签层级最高问题

显示弹窗时隐藏video

或者https://blog.csdn.net/qq_24510455/article/details/91974655

# js算术精度问题

big.js

# 自定义监听事件

const uidChange = new CustomEvent('uidChange', { uid: val }) window.dispatchEvent(uidChange)

window.addEventListener('uidChange', () => {xxxx})

# 换肤

一般方案

•方案一: 使用OOCSS模式,通过js动态切换公共类名来达到换肤效果
•方案二: 点击不同的按钮切换不同的样式表,如下:
        •theme-green.css
        •theme-red.css
        •theme-black.css
•方案三: localStorage存储主题,js动态获取本地存储换肤
•方案四: element和antd的动态换肤,需要实时编译style样式表
# 1. :target伪类
http://xuxi#home

<style>
    div:target {
        background: #06c;
    }
</style>
<a href="#home" >蓝</a>
<div id="bg1"></div>

通过锚点激活target

实例

<style>
    .bg {
      position: absolute;
      left: 0;
      top: 0;
      bottom: 0;
      right: 0;
    }
    .bg1 {
      z-index: 10;
      background-color: #000;
    }
    .bg2 {
      background-color: #06c;
    }
    .bg3 {
      background-color: #f0c;
    }
    .skin-btn-wrap {
      position: absolute;
      padding: 4px 10px;
      border-radius: 20px;
      line-height: 20px;
      background-color: #fff;
      z-index: 11;
      right: 20px;
      top: 20px;
    }
    .skin-btn-wrap a {
      display: inline-block;
      width: 20px;
      height: 20px;
      border-radius: 10px;
    }
    #one {
      background-color: #000;
    }
    #two {
      background-color: #06c;
    }
    #three {
      background-color: #f0c;
    }
    .bg:target {
      z-index: 10;
    }
    .bg:not(:target) {
      z-index: 0;
    }
  </style>

  <!-- css背景换肤 -->
  <div class="bg1 bg" id="bg1"></div>
  <div class="bg2 bg" id="bg2"></div>
  <div class="bg3 bg" id="bg3"></div>
  <div class="skin-btn-wrap">
    <a href="#bg1" id="one"></a>
    <a href="#bg2" id="two"></a>
    <a href="#bg3" id="three"></a>
  </div>

# add css

addCss (href) {
  const link = document.createElement('link')
  link.setAttribute('rel', 'stylesheet')
  link.setAttribute('type', 'text/css')
  link.setAttribute('href', href)
  document.getElementsByTagName('head')[0].appendChild(link)
}

removeCss (href) {
  const links = Array.from(document.getElementsByTagName('link'))
  links.forEach(link => {
    const linkHref = link.getAttribute('href')
    if (linkHref && linkHref === href) {
      link.parentNode.removeChild(link)
    }
  })
}

removeAllCss () {
  removeCss(`${process.env.VUE_APP_RES_URL}/theme/theme_default.css`)
  removeCss(`${process.env.VUE_APP_RES_URL}/theme/theme_eye.css`)
  removeCss(`${process.env.VUE_APP_RES_URL}/theme/theme_gold.css`)
  removeCss(`${process.env.VUE_APP_RES_URL}/theme/theme_night.css`)
}

# 加入购物车动画

# creata.js
// 全局创建组件实例函数
import Vue from 'vue'

export default function(Component, props){
  // 创建vue实例
  const instance = new Vue({
    render: h => { // vdom
      // console.log(h(Component, {props}));
      return h(Component, {props})
    }
  }).$mount();
  // 生成的dom追加至body中
  document.body.appendChild(instance.$el);
  // 添加一个销毁方法
  const comp = instance.$children[0];
  comp && (comp.remove = function () {
    document.body.removeChild(instance.$el)
    instance.$destroy();
  })

  return comp;
}

# 开始动画
      startCartAnim (el) {
        // 创建小球动画实例,开始动画
        const anim = this.$create(CartAnim, {
          pos: {right: '1.1rem', top: '0.2rem'},
          goods: this.goodsInfo
        })
        anim && anim.start(el)
        anim && anim.$on('transitionend', anim.remove)
      },
      
# 动画组件
<template>
  <div class="ball-wrap">
    <transition
      @before-enter="beforeEnter"
      @enter="enter"
      @afterEnter="afterEnter">
      <div class="ball" v-show="show" :style="pos">
        <div class="inner">
          <img :src="goods.image_default_id" alt="">
        </div>
      </div>
    </transition>
  </div>
</template>
<script>
  export default {
    name: 'CartAnim',
    data () {
      return { show: false }
    },
    props: ['pos', 'goods'],
    methods: {
      start (el) { // 启动动画接口,传递点击按钮元素
        this.el = el

      // 使.ball显示,激活动画钩子
        this.show = true
      },
      beforeEnter (el) {
        // 把小球移动到点击的dom元素所在位置
        const rect = this.el.getBoundingClientRect()
        // 转换为用于绝对定位的坐标
        // const x = rect.left - window.innerWidth / 2
        // const y = -(window.innerHeight - rect.top - 10 - 20)
        const x = -rect.left
        const y = rect.top - 100

      // ball只移动y
        el.style.transform = `translate3d(0, ${y}px, 0)`
      // inner只移动x
        const inner = el.querySelector('.inner')
        inner.style.transform = `translate3d(${x}px,0,0)`
      },
      enter (el, done) {
        // 获取offsetHeight就会重绘
        document.body.offsetHeight

      // 指定动画结束位置
        el.style.transform = `translate3d(0, 0, 0)`
        const inner = el.querySelector('.inner')
        inner.style.transform = `translate3d(0,0,0)`
        el.addEventListener('transitionend', done)
      },
      afterEnter (el) {
        // 动画结束,开始清理工作
        this.show = false
        el.style.display = 'none'
        this.$emit('transitionend')
      }
    }
  }
</script>
<style scoped lang="less">
  .ball-wrap {
    .ball {
      position: fixed;
      right: 1.1rem;
      top: 0.2rem;
      z-index: 100000;
      color: red;
      transition: all 0.5s cubic-bezier(0.49, -0.29, 0.75, 0.41);
      .inner {
        width: 0.7rem;
        height: 0.7rem;
        transition: all 0.5s linear;
        img {
          width: 0.7rem;
          height: 0.7rem;
          border-radius: 50%;
          overflow: hidden;
        }
      }
    }
  }
</style>

# 二、三级域名

那些www 开头的,其实都是二级域名。例如www.baidu.com,wenku.baidu.com,tieba,baidu.com,image.baidu.com等等 .xxx.com这样的都是二级 ,三级就是.*.xx.com

# Reflow、Repain

# 渲染页面流程

**1、解析HTML以构建DOM树:**渲染引擎开始解析HTML文档,转换树中的html标签或js生成的标签到DOM节点,它被称为 -- 内容树。 **2、构建渲染树:**解析CSS(包括外部CSS文件和样式元素以及js生成的样式),根据CSS选择器计算出节点的样式,创建另一个树 —- 渲染树。 3、布局渲染树: 从根节点递归调用,计算每一个元素的大小、位置等,给每个节点所应该出现在屏幕上的精确坐标。 4、绘制渲染树: 遍历渲染树,每个节点将使用UI后端层来绘制。

Reflow 计算出元素大小位置属性等 更耗资源

Repain 把元素画出来

# 引起变化操作
  1. 当你增加、删除、修改 DOM 结点时,会导致 Reflow 或 Repaint。
  2. 当你移动 DOM 的位置,或是搞个动画的时候。
  3. 当你修改 CSS 样式的时候。
  4. 当你 Resize 窗口的时候(移动端没有这个问题),或是滚动的时候。
  5. 当你修改网页的默认字体时。

注:display:none 会触发 reflow,而 visibility:hidden 只会触发 repaint,因为没有发现位置变化。

# 优化
  1. 不要一条一条地修改 DOM 的样式。与其这样,还不如预先定义好 cssclass,然后修改 DOMclassName

    // 不好的写法
    var left = 10,
    top = 10;
    el.style.left = left + "px";
    el.style.top  = top  + "px";
    // 推荐写法
    el.className += " theclassname";
    

    现代浏览器已经对连续修改style有优化,会合并到一次reflow中去。

  2. 把 DOM 离线后修改。如: a> 使用 documentFragment 对象在内存里操作 DOMb> 先把 DOMdisplay:none (有一次 repaint),然后你想怎么改就怎么改。比如修改 100 次,然后再把他显示出来。 c> clone 一个 DOM 节点到内存里,然后想怎么改就怎么改,改完后,和在线的那个的交换一下。

  3. 不要把 DOM 节点的属性值放在一个循环里当成循环里的变量。不然这会导致大量地读写这个结点的属性。

  4. 尽可能的修改层级比较低的 DOM节点。当然,改变层级比较底的 DOM节点有可能会造成大面积的 reflow,但是也可能影响范围很小。

  5. 为动画的 HTML 元件使用 fixedabsoultposition,那么修改他们的 CSS 是会大大减小 reflow

  6. 千万不要使用 table 布局。因为可能很小的一个小改动会造成整个 table 的重新布局。

https://segmentfault.com/a/1190000002629708

# 前端性能优化

# 1. 资源的合并与压缩

1.1 http 清求的过程及潜在的性能优化点

img

请求过程中一-些潜在的性能优化点

  • dns 是否可以通过缓存减少 dns 查询时间?
  • 网络请求的过程走最近的网络环境?
  • 相同的静态资源是否可以缓存?
  • 能否减少请求 http 请求大小?
  • 减少 http 请求
  • 服务端渲染

深入理解 http 请求的过程是前端性能优化的核心


  • 减少 http 请求数量
  • 减少请求资源的大小

1.2 html 压缩

HTML 代码压缩就是压缩这些在文本文件中有意义,但是在 HTML 中不显示的字符,包括空格,制表符,换行符等,还有一-些其他意义的字符,如 HTML 注释也可以被压缩。

img

如何进行 html 压缩:

  1. 使用在线网站进行压缩
  2. nodejs 提供了 html-minifier 工具
  3. 后端模板引擎渲染压缩

1.3 css 压缩

◆ 无效代码删除 ◆ css 语义合并

如何进行 css 压缩:

  1. 使用在线网站进行压缩
  2. 使用 html-minifier 对 html 中的 css 进行压缩
  3. 使用 clean-css 对 CSS 进行压缩

1.4 js 的压缩和混乱

◆ 无效字符的删除 ◆ 剔除注释 ◆ 代码语义的缩减和优化 ◆ 代码保护

如何进行 js 压缩和混乱:

  1. 使用在线网站进行压缩
  2. 使用 html-minifier 对 html 中的 js 进行压缩
  3. 使用 uglifyjs2 对 js 进行压缩

1.5 文件合并

img

文件合并存在的问题:

● 首屏渲染问题 ● 缓存失效问题

对于存在的问题,有以下建议

● 公共库合并 ● 不同页面的合并 ● 见机行事,随机应变

如何进行文件合并:

  1. 使用在线网站进行文件合并
  2. 使用 nodejs(webpackgulp) 实现文件合并

1.6 开启 gzip

小结

◆ web 前端的核心概念和 web 前端性能优化的意义所在 ◆ http 请求的过程及其中潜在的性能优化点 ◆ 压缩与合并的基本理念和使用 ◆ 在实战中体会和掌握本节课的内容在业务中的真实使用

# 2.图片相关的优化

2.1 JPG 图片的解析过程

img

2.2 png8/png24/png32 之间的区别

  • png8 一 256 色 + 支持透明
  • png24-- 2^24 色 + 不支持透明?
  • png32 一 2^24 色 + 支持透明

本质是文件大小、色彩丰富度不同

每种图片格式都有自己的特点,针对不同的业务场景选择不同的图片格式很重要

2.3 不同格式图片常用的业务场景

不同格式图片的特点

  • jpg 有损压缩,压缩率高,不支持透明
  • png 支持透明,浏览器兼容好
  • webp 压缩程度更好,在 ios webview 有兼容性问题
  • svg 矢量图 ,代码内嵌,相对较小,图片样式相对简单的场景

不同格式图片常用的业务场景

  • jpg 一 大部分不需要透明图片的业务场景
  • png 一 大部分需 要透明图片的业务场景
  • webp 一 安卓全部
  • svg 矢量图 一 图片样式相对简单的业务场景

2.4 图片压缩的几种方法

  • css 雪碧图

    把你的网站上用到的一些图片整合到一张单独的图片中

    优点:减少你的网站的 HTTP 请求数量 缺点:整合图片比较大时,一次加载比较慢

  • Image inline

    将图片的内容内嵌到 HTML 中(base64)

    优点:减少你的网站的 HTTP 请求数量

  • 使用矢量图

    使用 svg 进行矢量图的绘制、使用 iconfont 解决 icon 问题

  • 在安卓下使用 webp

    WebP 的优势体现在它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量;同时具备了无损和有损的压缩模式、Alpha 透明以及动画的特性,在 JPEG 和 PNG 上的转化效果都非常优秀、稳定和统一。

# 3. css 和 js 的装载和执行

3.1 HTML 页面加载渲染的过程

img

3.2 HTML 渲染过程的一些特点

  • 顺序执行、并发加载
  • 是否阻塞
  • 依赖关系
  • 引入方式

3.3 顺序执行、并发加载

  • 词法分析
  • 并发加载
  • 并发上限

3.4 css 阻塞和 js 阻塞

css 阻塞

  • css head 中阻塞页面的渲染
  • css 阻塞 js 的执行
  • css 不阻塞外部脚本的加载

js 阻塞

  • 直接引入的 js 阻塞页面的渲染
  • js 不阻塞资源的加载
  • js 是顺序执行,阻塞后续 js 逻辑的执行
# 4. 懒加载与预加载

4.1 懒加载原理

  • 图片进入可视区域之后请求图片资源
  • 对于电商等图片很多,页面很长的业务场景适用
  • 减少无效资源的加载
  • 并发加载的资源过多 会阻塞 js 的加载,影响网站的正常使用

当图片进入可视区域时,再去请求资源

需要监听 scroll 事件,在回调中判断图片是否进入可视区域

4.2 预加载原理

  • 图片等静态资源在使用之前的提前请求
  • 资源使用到时能从缓存中加载,提升用户体验
  • 页面展示的依赖关系维护
# 5. 重绘与回流

浏览器重绘与回流的机制

5.1 css 性能会让 javascript 变慢?

  • 一个线程 => JavaScript 解析
  • 一个线程 => UI 渲染

两个线程是互斥的

频繁触发重绘回流,会导致 UI 频繁渲染,最终导致 js 变慢

5.2 什么是重绘与回流

回流

  • 当 render tree 中的一 部分(或全部)因为元素的规模尺寸 ,布局隐藏等改变而需要重新构建。这就称为回流(reflow)
  • 页面布局几何属性改变时就需要回流

重绘

  • 当 render tree 中的- -些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如 background-color。则就叫称为重绘。

回流必将弓|起重绘,而重绘不一定会引起回流

5.3 避免重绘回流的两种方法

触发页面重布局的属性

  • 盒子模型相关属性会触发重布局
  • 定位属性及浮动也会触发重布局
  • 改变节点内部文字结构也会触发重布局

img

避免重绘回流的两种方法

  • 避免使用触发重绘、回流的 CSS 属性
  • 将重绘、回流的影响范围限制在单独的图层之内

只触发重绘的属性

img

新建 DOM 的过程

  1. 获取 DOM 后分割为多个图层
  2. 对每个图层的节点计算样式结果( Recalculate style--样式重计算)
  3. 为每个节点生成图形和位置( Layout--回流和重布局)
  4. 将每个节点绘制填充到图层位图中( Paint Setup 和 Paint--重绘)
  5. 图层作为纹理上传至 GPU
  6. 符合多个图层到页面上生成最终屏幕图像( Composite Layers--图层重组)

频繁重绘回流的 DOM 元素单独作为一个独立图层,那么这个 DOM 元素的重绘和回流的影响只会在这个图层中。


如何将 DOM 元素变成新的独立图层?

Chrome 创建图层的条件

  1. 3D 或透视变换 CSS 属性( perspective transform )
  2. 使用加速视频解码的<video>节点
  3. 拥有 3D ( WebGL )上下文或加速的 2D 上下文的<canvas>节点
  4. 混合插件(如 Flash )
  5. 对自己的 opacity 做 CSS 动画或使用一个动画 webkit 变换的元素
  6. 拥有加速 CSS 过滤器的元素
  7. 元素有一个包含复合层的后代节点(一个元素拥有一个子元素,该子元素在自己的层里)
  8. 元素有一个 z-index 较低且包含-个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)

注:图层(layers)不能滥用

实战优化点

  1. 用 translate 替代 top 改变
  2. 用 opacity 替代 visibility
  3. 不要一条-条地修改 DOM 的样式,预先定义好 class ,然后修改 DOM 的 className
  4. 把 DOM 离线后修改,比如:先把 DOM 给 display:none (有一次 Reflow) ,然后你修改 100 次,然后再把它显示出来
  5. 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量
  6. 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
  7. 动画实现的速度的选择
  8. 对于动画新建图层
  9. 启用 GPU 硬件加速

# 分享代码插件carbon

# axios超时拦截

axios.defaults.timeout = 30000;
axios.interceptors.response.use(res => {
    return res
}, error => {
    if (error.code === 'ECONNABORTED' || error.message.includes('timeout') || error.message === 'Network Error') {
        Vue.prototype.showToast('连接超时,请稍后再试')
    }
})

# 手写XMlHttpRequest

util.json = function (options) {
  var opt = {
    url: '',
    type: 'get',
    data: {},
    success: function () { },
    error: function () { },
  };
  util.extend(opt, options);
  if (opt.url) {
    var xhr = XMLHttpRequest
      ? new XMLHttpRequest()
      : new ActiveXObject('Microsoft.XMLHTTP');
    var data = opt.data,
      url = opt.url,
      type = opt.type.toUpperCase(),
      dataArr = [];
    for (var k in data) {
      dataArr.push(k + '=' + data[k]);
    }
    if (type === 'GET') {
      url = url + '?' + dataArr.join('&');
      xhr.open(type, url.replace(/\?$/g, ''), true);
      xhr.send();
    }
    if (type === 'POST') {
      xhr.open(type, url, true);
      xmlhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
      xhr.send(dataArr.join('&'));
    }
    xhr.onload = function () {
      if (xhr.status === 200 || xhr.status === 304) {
        var res;
        if (opt.success && opt.success instanceof Function) {
          res = xhr.responseText;
          if (typeof res ==== 'string') {
            res = JSON.parse(res);
            opt.success.call(xhr, res);
          }
        }
      } else {
        if (opt.error && opt.error instanceof Function) {
          opt.error.call(xhr, res);
        }
      }
    };
  }
};

# 前端领域模式

说的高大上,其实就是组件复用,不是很看好,需求不同导致样式不一致,为了统一组件强行写在一起只会让代码越来越复杂,上手难度也更高

不过如果项目时后台管理系统这种,样式变化不大,倒是可以这么做。像ext.js就是一套成熟的系统。

# 同时多个加减号

2 + - + - + + - - + 3 = 5

看减号的数量,负负得正,加号不用管

# 工具函数

# 常用工具函数

https://mp.weixin.qq.com/s/ng_j-X72TuLELEuXMMSatA

# rgb与16进制互转

const hex2Rgb = (sColor, rgba, opacity) => {
  // 16进制颜色转
  rgba = rgba ? rgba : 'rgb'
  sColor = sColor.toLowerCase()
  if (sColor) {
    if (sColor.length === 4) {
      let sColorNew = '#'
      for (let i = 1; i < 4; i += 1) {
        sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1))
      }
      sColor = sColorNew
    }
    // 处理六位的颜色
    let sColorChange = []
    for (let i = 1; i < 7; i += 2) {
      sColorChange.push(parseInt('0x' + sColor.slice(i, i + 2)))
    }
    return `${rgba}(${sColorChange.join(',')}, ${opacity})`
  } else {
    return sColor
  }
}

const rgb2hex = (rgb) => {
  // rgb转16进制
  if (rgb.charAt(0) === '#') return rgb
  let ds = rgb.split(/\D+/)
  let decimal = Number(ds[1]) * 65536 + Number(ds[2]) * 256 + Number(ds[3])
  return '#' + zeroFillHex(decimal, 6)
}

const zeroFillHex = (num, digits) => {
  // rgb转16进制附带
  let s = num.toString(16)
  while (s.length < digits) {
    s = '0' + s
  }
  return s
}

# url截取图片名字

function getImgName (path) {
	return path.substring(path.lastIndexOf('/') + 1, path.length)
}

# 获取字符串长度

getByteLen (val) {
  let len = 0
  for (let i = 0; i < val.length; i++) {
    let a = val.charAt(i)
    if (a.match(/[^\x00-\xff]/ig) != null) { // \x00-\xff→GBK双字节编码范围
      len += 2
    } else {
      len += 1
    }
  }
  return len
}

# img url to base64

convertImgToBase64 (url, callback, outputFormat) {
  // img url to base64
  let canvas = document.createElement('CANVAS')
  let ctx = canvas.getContext('2d')
  let img = new Image()
  img.crossOrigin = 'Anonymous'
  img.onload = function () {
    canvas.height = img.height
    canvas.width = img.width
    ctx.drawImage(img, 0, 0)
    let dataURL = canvas.toDataURL(outputFormat || 'image/png')
    callback.call(this, dataURL)
    canvas = null
  }
  img.src = url
},

# 日期数字转大小写

numberToCap (y, m) {
  // 日期数字转大写
  let chinese = ['〇', '一', '二', '三', '四', '五', '六', '七', '八', '九']
  let result = ''
  for (let i = 0; i < y.length; i++) {
    result += chinese[y.charAt(i)]
  }
  result += '年'
  if (m.length === 2) {
    if (m.charAt(0) === '1') {
      result += ('十' + chinese[m.charAt(1)] + '月')
    }
  } else {
    result += (chinese[m.charAt(0)] + '月')
  }
  // if (d.length === 2) {
  //   result += (chinese[d.charAt(0)] + '十' + chinese[d.charAt(1)] + '日')
  // } else {
  //   result += (chinese[d.charAt(0)] + '日')
  // }
  return result
},
numberToSmall (y, m) {
  // 日期数字转小写
  let chinese = ['〇', '一', '二', '三', '四', '五', '六', '七', '八', '九']
  let number = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
  let result = ''
  for (let i = 0; i < y.length; i++) {
    let index = chinese.findIndex((item) => {
      return item === y.charAt(i)
    })
    result += number[index]
  }
  result += '年'
  if (m.length === 2) {
    if (m.charAt(0) === '十') {
      let index = chinese.findIndex((item) => {
        return item === m.charAt(1)
      })
      result += '1' + number[index] + '月'
    }
  } else {
    let index = chinese.findIndex((item) => {
      return item === m.charAt(0) + '月'
    })
    result += number[index] + '月'
  }
  // if (d.length === 2) {
  //   result += (chinese[d.charAt(0)] + '十' + chinese[d.charAt(1)] + '日')
  // } else {
  //   result += (chinese[d.charAt(0)] + '日')
  // }
  return result
},
clickCapFont () {
  // 数字大写
  if (this.capNav === 1) {
    this.capNav = 0
    let date = this.competitionTime.replace('年', ' ').replace('月', '').split(' ')
    this.competitionTime = this.numberToSmall(date[0], date[1])
  } else {
    this.capNav = 1
    let date = this.competitionTime.replace('年', ' ').replace('月', '').split(' ')
    this.competitionTime = this.numberToCap(date[0], date[1])
  }
},

# backToRefresh

activated () {
  const backToRefresh = localStorage.getItem('backToRefresh')
  if (backToRefresh) {
    this.backToRefreshLogic(JSON.parse(backToRefresh))
  }
},
backRefresh (e) {
  this.backToRefreshLogic(e.detail.backToRefresh)
},
backToRefreshLogic (backData) {
  if (backData) {
    // 刷新数据
    const type = backData.type
    const data = backData.data
    if (type === 'refreshRecommend') {
      this.content = data
      this.refreshData()
    }
    this.clearBackToRefresh()
  }
},
if (this.isGreenApp) {
  window.addEventListener('onShowEvent', this.backRefresh)
  this.$on('hook:beforeDestroy', () => {
    window.removeEventListener('onShowEvent', this.backRefresh)
  })
}
beforeDestroy () {
  window.removeEventListener('onShowEvent', this.backRefresh)
},

# 复制到粘贴板

copy (Url2) {
  let oInput = document.createElement('input')
  oInput.value = Url2
  document.body.appendChild(oInput)
  oInput.select() // 选择对象
  document.execCommand('Copy') // 执行浏览器复制命令
  oInput.className = 'oInput'
  oInput.style.display = 'none'
  this.showToast('复制成功')
},

或者

/**
 * @method 复制内容到粘贴板
 * @input data:要复制的内容   toast: 要提示的信息 可不传
 */
function copyData (data, toast) {
  toast = toast || '复制成功'
  if (Vue.prototype.isGreenApp) {
    this.$sendDataToClient({
      type: 'copyToPasteboard',
      url: data
    })
    Vue.prototype.showToast(toast)
  } else {
    let node = document.createElement('div')
    node.id = 'copy'
    node.innerHTML = data
    node.style.opacity = '0'
    document.body.appendChild(node)
    let pathItem = document.querySelector('#copy')
    let range = document.createRange()
    range.selectNode(pathItem)
    window.getSelection().removeAllRanges()
    window.getSelection().addRange(range)
    if (document.execCommand('copy')) {
      Vue.prototype.showToast(toast)
    }
    document.body.removeChild(node)
    window.getSelection().removeAllRanges()
  }
}

# 根据出生日期算出年龄

// 根据出生日期算出年龄
function jsGetAge (strBirthday) {
  let returnAge
  let strBirthdayArr = strBirthday.split('-')
  let birthYear = strBirthdayArr[0]
  let birthMonth = strBirthdayArr[1]
  let birthDay = strBirthdayArr[2]

  let d = new Date()
  let nowYear = d.getFullYear()
  let nowMonth = d.getMonth() + 1
  let nowDay = d.getDate()

  if (nowYear === birthYear) {
    returnAge = 0// 同年 则为0岁
  } else {
    let ageDiff = nowYear - birthYear // 年之差
    if (ageDiff > 0) {
      if (nowMonth === birthMonth) {
        let dayDiff = nowDay - birthDay// 日之差
        if (dayDiff < 0) {
          returnAge = ageDiff - 1
        } else {
          returnAge = ageDiff
        }
      } else {
        let monthDiff = nowMonth - birthMonth// 月之差
        if (monthDiff < 0) {
          returnAge = ageDiff - 1
        } else {
          returnAge = ageDiff
        }
      }
    } else {
      returnAge = -1 // 返回-1 表示出生日期输入错误 晚于今天
    }
  }
  return returnAge // 返回周岁年龄
}

# 判断对象是否相等

diff (obj1, obj2) {   // 判断对象是否相等
  var o1 = obj1 instanceof Object
  var o2 = obj2 instanceof Object
  if (!o1 || !o2) {
    return obj1 === obj2
  }
  if (Object.keys(obj1).length !== Object.keys(obj2).length) {
    return false
  }
  for (var attr in obj1) {
    var t1 = obj1[attr] instanceof Object
    var t2 = obj2[attr] instanceof Object
    if (t1 && t2) {
      return this.diff(obj1[attr], obj2[attr])
    } else if (obj1[attr] !== obj2[attr]) {
      return false
    }
  }
  return true
},

# 拿url参数

getQuery (variable) {
  var query = window.location.href.split('?')[1]
  var vars = query.split('&')
  for (var i = 0; i < vars.length; i++) {
    var pair = vars[i].split('=')
    if (pair[0] === variable) {
      return pair[1]
    }
  }
  return (false)
},

在hash模式下用search拿不到

# 数组去重

// 数组去重  往后面找
const distinct = (arr) => {
  if (!(arr instanceof Array)) {
    throw new Error('no Array')
  }
  let result = []
  arr.forEach((v, i, arr) => {
    let bool = arr.indexOf(v, i + 1)
    if (bool === -1) {
      result.push(v)
    }
  })
  return result
}

# 校验身份证

/**
 * @method 校验身份证
 * @input code 身份证号
 * @return {boolean}
 */
function identityCodeValid (code) {
  let city = {
    11: '北京',
    12: '天津',
    13: '河北',
    14: '山西',
    15: '内蒙古',
    21: '辽宁',
    22: '吉林',
    23: '黑龙江 ',
    31: '上海',
    32: '江苏',
    33: '浙江',
    34: '安徽',
    35: '福建',
    36: '江西',
    37: '山东',
    41: '河南',
    42: '湖北 ',
    43: '湖南',
    44: '广东',
    45: '广西',
    46: '海南',
    50: '重庆',
    51: '四川',
    52: '贵州',
    53: '云南',
    54: '西藏 ',
    61: '陕西',
    62: '甘肃',
    63: '青海',
    64: '宁夏',
    65: '新疆',
    71: '台湾',
    81: '香港',
    82: '澳门',
    91: '国外 '
  }
  let tip = ''
  let pass = true

  if (!code || !/^\d{6}(18|19|20)?\d{2}(0[1-9]|1[012])(0[1-9]|[12]\d|3[01])\d{3}(\d|X)$/i.test(code)) {
    tip = '身份证号格式错误'
    pass = false
  } else if (!city[code.substr(0, 2)]) {
    tip = '地址编码错误'
    pass = false
  } else {
    // 18位身份证需要验证最后一位校验位
    if (code.length === 18) {
      code = code.split('')
      // ∑(ai×Wi)(mod 11)
      // 加权因子
      let factor = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
      // 校验位
      let parity = [1, 0, 'X', 9, 8, 7, 6, 5, 4, 3, 2]
      let sum = 0
      let ai = 0
      let wi = 0
      for (let i = 0; i < 17; i++) {
        ai = code[i]
        wi = factor[i]
        sum += ai * wi
      }
      // let last = parity[sum % 11]
      if (+parity[sum % 11] !== +code[17]) {
        tip = '身份证输入有误'
        pass = false
      }
    }
  }
  if (tip) Vue.prototype.showToast(tip)
  return pass
}

# 图片加水印

/**
 *
 * @param {*} image 图片 img对象
 * @param {*} words 水印内容
 */
export default function(image, words) {
  const { canvas, context } = getCanvas()
  let width = image.width
  let height = image.height
  canvas.width = width
  canvas.height = height
  canvas.style.width = image.style.width
  canvas.style.height = image.style.height

  context.drawImage(image, 0, 0)

  // 重复绘制内容贴图
  const mark = getMark(words)
  for (let i = 0; i < width; i += 250) {
    for (let j = 0; j < height; j += 250) {
      context.drawImage(mark, i, j)
    }
  }
  return toImage(canvas)
}

function toImage(canvas) {
  var image = new Image()
  image.src = canvas.toDataURL('image/png')
  return image
}

// 构造内容
function getMark(content = '') {
  const { canvas, context } = getCanvas()
  canvas.width = 200
  canvas.height = 200
  context.translate(100, 100)
  context.rotate((45 * Math.PI) / 180)
  context.font = '30px 微软雅黑'
  context.textAlign = 'center'
  context.fillStyle = 'rgba(220,20,60, 0.005)'

  const words = content.split('\n') // 以\n为换行
  const lines = words.length
  const fontHeight = context.measureText('田').width * 1.1
  const wordsHeight = fontHeight * lines
  const start = -wordsHeight / 2
  for (let i = 0; i < lines; i++) {
    context.fillText(words[i], 0, start + i * fontHeight)
  }
  return canvas
}

function getCanvas() {
  const canvas = document.createElement('canvas')
  return {
    canvas,
    context: canvas.getContext('2d')
  }
}

# 生成随机密码并显示强度

/**
/*方法说明
 *@createPassword 密码范围 {0-9,A-Z, a-z}
 *@param
 * num {num} 生成密码长度
 *  b {num} 密码生成类型,1: 数字, 2:数字+小写,3.数字+大小写
 *@return {str} 
*/
**/
let createPassword = function (num, b) {
    let n = 0 //循环次数
    let arr = new Array // 随机数保存
    let Capitalization = () => Math.floor(Math.random() * b)// 随机取数0-2/0-1
    let randomNumber = [() => Math.floor(Math.random() * (57 - 48 + 1) + 48), () => Math.floor(Math.random() * (122 - 97 + 1) + 97), () => Math.floor(Math.random() * (90 - 65 + 1) + 65)] // 去随机值
    for (let i = 0; i < num; i++) {
        arr.push(randomNumber[Capitalization()]())
    }
    return arr.map(e => String.fromCharCode(e)).join('')
}

/*方法说明
 *@method passwordStrength 密码强度校验
 *@for 所属类名
 *@param 
 *        num{str} 密码
 *        len {num} 密码长度
 *@return {str} 返回密码等级

      str    ASCII
      0-9    48-57
      A-Z    65-90
      a-z    97-122

 */
let passwordStrength = function (num, len) {
    if(num.length<len) return '密码长度不够'
    let passwordLevel = ['弱', '普通', '较强', '强']
    let arr = new Array(4) // 记录密码等级
    num.split('').map(e => e.charCodeAt()).forEach(e => {
        if (e >= 48 && e <= 57) { arr[0] = 1 }
        else if (e >= 65 && e <= 90) { arr[1] = 1 }
        else if (e >= 91 && e <= 122) { arr[2] = 1 }
        else { arr[3] = 1 }
    })
    return passwordLevel[arr.reduce((a, b) => a + b) - 1]
}

# 找到字符串中某字符出现的次数

str.split('xx').length - 1

# JS 格式化数字(每三位加逗号)

function toThousands (num = 0) {
    num = num.toString()
    let result = ''
    while (num.length > 3) {
      result = ',' + num.slice(-3) + result
      num = num.slice(0, num.length - 3)
    }
    if (num) result = num + result
    return result
  }

# 判断设备

let ua = navigator.userAgent
Vue.prototype.IMAPPSDKID = 1400253073 // 统一的imSdkId
Vue.prototype.topBarHeight = '1.04rem'
Vue.prototype.isWx = (/MicroMessenger/i).test(ua)   // 微信浏览器
Vue.prototype.isWindowsWx = (/MicroMessenger/i).test(ua) && (/WindowsWechat/i).test(ua)   // 微信电脑端浏览器
Vue.prototype.isQQBrowser = (/QQ/i).test(ua) && !(/MicroMessenger/i).test(ua)   // 手机QQ浏览器
Vue.prototype.isAndroid = (/(Android)/i).test(ua) // android浏览器
Vue.prototype.isIOS = (/(iPhone|iPad|iPod|iOS)/i).test(ua)  // iOS 浏览器
Vue.prototype.isGreenApp = (window.eventListener) || (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.eventListener) // 绿茵场APP内
Vue.prototype.isWxApp = (/miniProgram/i).test(ua) || window.__wxjs_environment === 'miniprogram'
// 判断是不是电脑
const platform = navigator.platform.toLowerCase()
Vue.prototype.isPc = platform.indexOf('win') !== -1 || platform.indexOf('mac') !== -1

# 获取字符串长度

getByteLen (val) {
  // 获取字符串长度
  let len = 0
  for (let i = 0; i < val.length; i++) {
    let a = val.charAt(i)
    if (a.match(/[^\x00-\xff]/ig) != null) { // \x00-\xff→GBK双字节编码范围
      len += 2
    } else {
      len += 1
    }
  }
  return len
},

# 获取屏幕放大比例

/**
 * 获取屏幕放大比例
 * @returns {number}
 */
SF.detectZoom = function () {
	var ratio = 0,
		screen = window.screen,
		ua = navigator.userAgent.toLowerCase();

	if (window.devicePixelRatio !== undefined) {
		ratio = window.devicePixelRatio;
	}
	else if (~ua.indexOf('msie')) {
		if (screen.deviceXDPI && screen.logicalXDPI) {
			ratio = screen.deviceXDPI / screen.logicalXDPI;
		}
	}
	else if (window.outerWidth !== undefined && window.innerWidth !== undefined) {
		ratio = window.outerWidth / window.innerWidth;
	}

	if (ratio) {
		ratio = Math.round(ratio * 100);
	}
	return ratio;
};

# 插件

# VConsole

根据条件自行加载

let script = document.createElement('script')
script.src = '//share.greenplayer.cn/share/sharepage/js/lib/vconsole.min.js'
script.onload = () => {
  if (window.VConsole) {
    /* eslint-disable no-new */
    new window.VConsole()
  }
}
document.head.appendChild(script)

# qs

const qs = require('qs')

qs.stringify		用&拼接
qs.parseInt			键值对

对url进行操作 

# 骨架屏

npm i vue-content-loader

import { ContentLoader } from 'vue-content-loader'

components: {
  ContentLoader
},

再到这网站去编辑样式,复制代码,完事
http://danilowoz.com/create-vue-content-loader/

# 小数操作

问题原因:

在 JavaScript 中所有数值都以 IEEE-754 标准的 64 bit 双精度浮点数进行存储的

有效精度位共53位,超过就存在精度丢失

  1. 使用decimal.js

  2. function addNum (num1, num2) {
           var sq1,sq2,m;
           try {
             sq1 = num1.toString().split(".")[1].length;
           }
           catch (e) {
             sq1 = 0;
           }
           try {
             sq2 = num2.toString().split(".")[1].length;
           }
           catch (e) {
             sq2 = 0;
           }
           m = Math.pow(10,Math.max(sq1, sq2));
           return (num1 * m + num2 * m) / m;
         }
         alert(addNum(0.1, 0.2));