JavaScript核心知识总结(中)

前言

在上篇中,我们总结了 JavaScript 这门语言的基础知识,而这篇则是讲述这门语言的特色,也是它的核心知识、面试重点。

函数

函数可谓是JavaScript中的一等公民,与函数涉及的也有相当多的概念,当初笔者刚学的时候被绕的云里雾里,下面先以一个问题,开始函数的总结与学习。

怎么执行一段JavaScript代码

JavaScript 代码的执行主要分为以下三步:

  • 分析有没有词法、语法错误
  • 预编译-发生在函数执行的前一刻,生成变量环境、词法环境
  • 解释执行

JavaScript 比较有特色的就是在预编译阶段,我们重点看看预编译阶段做了什么事情。

预编译

引擎一开始会创建执行上下文(也叫Activation ObjectAO对象),执行上下文主要有如下三种类型:

  • 全局执行上下文:只有一个
  • 函数执行上下文:存在无数个,每个函数被调用就新建一个
  • Eval执行上下文:eval中运行的函数代码,很少用

执行上下文的创建主要分为创建阶段和执行阶段

创建阶段

1.绑定 this 指向

2.创建词法环境

3.生成变量环境

这里解释一下词法环境和变量环境,其实他们两个是差不多相同的组件。词法环境中包含两个部分,一个是存储变量与函数声明的位置,另一个是对外部环境的引用

伪代码如下:

GlobalExectionContext = {
       // 全局执行上下文
  LexicalEnvironment: {
         	  // 词法环境
    EnvironmentRecord: {
        		// 环境记录
      Type: "Object",      		   // 全局环境
      // 标识符绑定在这里 
      outer: <null>, // 对外部环境的引用
    }  	   		  
  }  
}

FunctionExectionContext = {
      // 函数执行上下文
  LexicalEnvironment: {
       	  // 词法环境
    EnvironmentRecord: {
       		// 环境记录
      Type: "Declarative",  	   // 函数环境
      // 标识符绑定在这里 			  // 对外部环境的引用
      outer: <Global or outer function environment reference>  
  }  
}

变量环境也是一个词法环境,词法环境和变量环境的区别在于:

  • 词法环境存储函数声明和绑定 letconst 变量
  • 变量环境仅绑定 var 变量

执行阶段

完成对所有变量的分配,最后执行代码

作用域&作用域链

每个 JavaScript 函数都是一个对象,对象中有的属性可以访问,有的不能,这些属性仅供 JavaScript 引擎存取,如[[scope]]

[[scope]]就是函数的作用域,其中存储了执行上下文的集合

[[scope]]中所存储的执行上下文对象的集合,这个集合呈链式链接,我们称这种链式链接为作用域链。查找变量时,要从作用域链的顶部开始查找。在当前执行上下文中找不到变量时,则到对外部环境的引用中向上查找,故呈现一个链式结构。

作用域与变量声明提升

  • JavaScript 中,函数声明与变量声明会被 JavaScript 引擎隐式地提升到当前作用域的顶部
  • 声明语句中的赋值部分并不会被提升,只有名称被提升
  • 函数声明的优先级高于变量,如果变量名跟函数名相同且未赋值,则函数声明会覆盖变量声明
  • 如果函数有多个同名参数,那么最后一个参数(即使没有定义)会覆盖前面的同名参数

闭包

当内部函数被保存到外部时,将会生成闭包。生成闭包后,内部函数依旧可以访问其所在的外部函数的变量。

当函数执行时,会创建执行上下文,获取作用域链(存储了函数能够访问的所有执行上下文)。函数每次执行时对应的执行上下文都是独一无二的,当函数执行完毕,函数都会失去对这个作用域链的引用, JS 的垃圾回收机制是采用引用计数策略,如果一块内存不再被引用了那么这块内存就会被释放。

但是,当闭包存在时,即内部函数保留了对外部变量的引用时,这个作用域链就不会被销毁,此时内部函数依旧可以访问其所在的外部函数的变量,这就是闭包。

即闭包逃过了 GC 策略,故滥用会导致内存泄漏,其实本身就是一种内存泄漏?

经典题目

for (var i = 0; i < 5; i++) {
     
  setTimeout(function timer() {
     
    console.log(i)
  }, i * 100)
}
function test() {
     
   var a = [];
   for (var i = 0; i < 5; i++) {
     
         a[i] = function () {
     
            console.log(i);
         }
   }
   return a;
}

var myArr = test();
for(var j=0;j<5;j++)
{
     
   myArr[j]();
}

以上两个例子都打印5个5,简单解释就是变量 i 记录的是最终跳出循环的值,即5,可以通过立即执行函数或者 let 来解决。因为立即执行函数创建了一个新的执行上下文,可以保存当前循环 i 的值,而let则构建了块级作用域,也可以保存当前循环 i 的值。

for (var i = 0; i < 5; i++) {
     
   ;(function(i) {
     
      setTimeout(function timer() {
     
         console.log(i)
      }, i * 100)
   })(i)
}
function test(){
     
   var arr=[];
   for(i=0;i<10;i++)
   {
     
      (function(j){
     
         arr[j]=function(){
     
         console.log(j);
         }
      })(i)
   }
   return arr;
}

var myArr=test();
for(j=0;j<10;j++)
{
     
   myArr[j]();
}

封装私有变量

function Counter() {
     
   let count = 0;
   this.plus = function () {
     
      return ++count;
   }
   this.minus = function () {
     
      return --count;
   }
   this.getCount = function () {
     
      return count;
   }
}

const counter = new Counter();
counter.puls();
counter.puls();
console.log(counter.getCount())

计数器

实现一个foo函数 可以这么使用:

a = foo();
b = foo();
c = foo();
// a === 1;b === 2;c === 3;
foo.clear();d = foo(); //d === 1;
function myIndex() {
     
    var index = 1;

    function foo(){
     
        return index++;
    }

    foo.clear = function() {
     
        index = 1;
    }
    return foo;
}

var foo = myIndex();

JavaScript 中,调用函数的方式?

JavaScript 中,调用函数的方式主要有如下数种

  • 方法调用模式 Foo.foo(arg1, arg2) ;
  • 函数调用模式 foo(arg1, arg2);
  • 构造器调用模式 (new Foo())(arg1, arg2);
  • call / apply 调用模式 Foo.foo.call(that, arg1, arg2);
  • bind 调用模式 Foo.foo.bind(that)(arg1, arg2)();

防抖节流

无论是面试还是业务开发,这都是经常接触到的知识,我们一起来看看

防抖 debounce

函数防抖就是在函数需要频繁触发的情况下,只有足够的空闲时间,才执行一次。

典型应用

  • 百度搜索框在输入稍有停顿时才更新推荐热词。
  • 拖拽
function debounce(handler, delay = 300){
     

  var timer = null;

  return function(){
     

    var _self = this,
        _args = arguments;

    clearTimeout(timer);
    timer = setTimeout(function(){
     
      handler.apply(_self, _args);
    }, delay);
  }
// 频繁触发时,清除对应的定时器,然后再开一个定时器,delay秒后执行
function debounce(handler, delay){
     

  delay = delay || 300;
  var timer = null;

  return function(){
     

    var _self = this,
        _args = arguments;

    clearTimeout(timer);
    timer = setTimeout(function(){
     
      handler.apply(_self, _args);
    }, delay);
  }
}

// 不希望被频繁调用的函数
function add(counterName) {
     
  console.log(counterName + ":  " + this.index ++);
}

// 需要的上下文对象
let counter = {
     
  index: 0
}

// 防抖的自增函数,绑定上下文对象counter
let db_add = debounce(add, 10).bind(counter)

// 每隔500ms频繁调用3次自增函数,但因为防抖的存在,这3次内只调用一次
setInterval(function() {
     
  db_add("someCounter1");
  db_add("someCounter2");
  db_add("someCounter3");
}, 500)


/**
 * 预期效果:
 * 
 * 每隔500ms,输出一个自增的数
 * 即打印:
    someCounter3:  0
    someCounter3:  1
    someCounter3:  2
    someCounter3:  3
 */

节流 throttle

一个函数只有在大于执行周期时才执行,周期内调用不执行。好像水滴积攒到一定程度才会触发一次下落一样。

典型应用:

  • 抢券时疯狂点击,既要限制次数,又要保证先点先发出请求
  • 窗口调整
  • 页面滚动
function throttle(fn,wait=300){
     
    var lastTime = 0
    return function(){
     
        var that = this,args=arguments
        var nowTime = new Date().getTime()
        if((nowTime-lastTime)>wait){
     
            fn.apply(that,args)
            lastTime = nowTime
        }
    }
}

this

在上面说函数的时候,我们也提到了一下 this ,即函数创建执行上下文的时候第一步就是绑定 this 的指向,也对应了那句话-- JavaScriptthis 的指向是当函数执行的时候才确定的。

this 的指向主要有如下数种:

  1. 作为函数直接调用,非严格模式下,this指向window,严格模式下,this指向undefined
  2. 作为某对象的方法调用,this通常指向调用的对象
  3. 使用applycallbind可以绑定this指向
  4. 在构造函数中,this指向新创建的对象
  5. 箭头函数没有单独的this值,this在箭头创建时绑定,它与声明所在的上下文相同

当多个this出现时,this改指向哪里?

首先, new 的方式优先级最高,接下来是 bind 这些函数,然后是 obj.foo() 这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。

  1. new 绑定
  2. 显式绑定-> bindapplycall
  3. 隐式绑定-> obj.foo()
  4. 默认绑定-> 浏览器环境默认是window

默认绑定

function foo() {
      // 运行在严格模式下,this会绑定到undefined
    "use strict";
    console.log( this.a );
}

var a = 2;

// 调用
foo(); // TypeError: Cannot read property 'a' of undefined

// --------------------------------------

function foo() {
      // 运行
    console.log( this.a );
}

var a = 2;
foo()//2

隐式绑定

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

var obj = {
     
    a: 2,
    foo: foo
};

obj.foo(); // 2

注意下面这种情况,称为隐式丢失。

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

var obj = {
     
    a: 2,
    foo: foo
};

var bar = obj.foo; // 函数别名

var a = "global"; // a是全局对象的属性

bar(); // "global"

显示绑定

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

var obj = {
     
    a: 2
};

foo.call( obj ); // 2  调用foo时强制把foo的this绑定到obj上

new 绑定

function foo(a) {
     
    this.a = a;
}

var bar = new foo(2); // bar和foo(..)调用中的this进行绑定
console.log( bar.a ); // 2

某厂面试

请分别写出下面题目的答案。

function Foo() {
     
    getName = function() {
     
        console.log(1);
    };
    return this;
}
Foo.getName = function() {
     
    console.log(2);
};
Foo.prototype.getName = function() {
     
    console.log(3);
};
var getName = function() {
     
    console.log(4);
};

function getName() {
     
    console.log(5);
}

//请写出以下输出结果:
Foo.getName();      //-> 2    Foo对象上的getName() ,这里不会是3,因为只有Foo的实例对象才会是3,Foo上面是没有3的
getName();          //-> 4    window上的getName,console.log(5)的那个函数提升后,在console.log(4)的那里被重新赋值
Foo().getName();    //-> 1    在Foo函数中,getName是全局的getName,覆盖后输出 1
getName();          //-> 1    window中getName();
new Foo.getName();  //-> 2    Foo后面不带括号而直接 '.',那么点的优先级会比new的高,所以把 Foo.getName 作为构造函数
new Foo().getName();//-> 3    此时是Foo的实例,原型上会有输出3这个方法

箭头函数中的this判断

箭头函数里面的 this 是继承它作用域父级的 this , 即声明箭头函数处的 this

let a = {
     
  b: function() {
     
    console.log(this)
  },
  c: () => {
     
    console.log(this)
  }
}

a.b()   // a
a.c()   // window

let d = a.b
d()     // window

bind、apply实现

自封装 bind 方法

  • 因为 bind 的使用方法是 某函数.bind(某对象,…剩余参数)
    • 所以需要在Function.prototype 上进行编程
  • 将传递的参数中的某对象和剩余参数使用 apply 的方式在一个回调函数中执行即可
  • 要在第一层获取到被绑定函数的 this ,因为要拿到那个函数用 apply
/**
 * 简单版本
 */
Function.prototype.myBind = (that, ...args) => {
     
  const funcThis = this;
  return function(..._args) {
     
    return funcThis.apply(that, args.concat(_args));
  }
}

Function.prototype.mybind = function(ctx) {
     
    var _this = this;
    var args = Array.prototype.slice.call(arguments, 1);
    return function() {
     
        return _this.apply(ctx, args.concat(args, Array.prototype.slice.call(arguments)))
    }
}
/**
 * 自封装bind方法
 * @param  {对象} target [被绑定的this对象, 之后的arguments就是被绑定传入参数]
 * @return {[function]}  [返回一个新函数,这个函数就是被绑定了this的新函数]
 */
Function.prototype.myBind = function (target){
     
	target = target || window;
	var self = this;
	var args = [].slice.call(arguments, 1);
	var temp = function(){
     };
	var F = function() {
     
    var _args = [].slice.call(arguments, 0);
		return self.apply(this instanceof temp ? this: target, args.concat(_args));
	}
	temp.prototype = this.prototype;    //当函数是构造函数时,维护原型关系
	F.prototype = new temp();
	return F;
}

自封装一个apply

  • 首先要先原型上即 Function.prototype 上编程
  • 需要拿到函数的引用, 在这里是 this
  • 让 传入对象.fn = this
  • 执行 传入对象.fn(传入参数)
  • 返回执行结果
Function.prototype.myApply = function(context) {
     
  if (typeof this !== 'function') {
     
    throw new TypeError('Error')
  }
  context = context || window
  context.fn = this
  let result
  // 处理参数和 call 有区别
  if (arguments[1]) {
     
    result = context.fn(...arguments[1])
  } else {
     
    result = context.fn()
  }
  delete context.fn
  return result
}

new实现

new的过程

  • 新生成一个对象
  • 链接到原型
  • 绑定this
  • 返回新对象
function create() {
     
    let obj = {
     }
    obj.__proto__ = con.prototype
    con.call(this)
    return obj
};

原型链

  • JavaScript所有对象都有一个__proto__属性,这个属性所对应的就是该对象的原型
  • JavaScript的函数对象除了原型__proto__之外,还预置了prototype属性(Function.prototype.bind没有)
  • 当函数对象作为构造函数创建实例时,其prototype属性值将被作为实例对象的原型__proto__

constructor

constructor 返回创建实例对象时构造函数的引用

function Parent(age) {
     
    this.age = age;
}

var p = new Parent(50);
p.constructor === Parent; // true
p.constructor === Object; // false

prototype

这是一个显式原型属性,只有函数才拥有该属性。基本所有的函数都有这个属性,除了Function.prototype.bind()

let fun = Function.prototype.bind()

如果你以上述方法创建一个函数,那么可以发现这个函数是不具有 prototype 属性的。

prototype是如何产生的

当我们声明一个函数时,这个属性就被自动创建了

function Foo(){}

并且这个属性的值是一个对象(也就是原型),只有一个属性constructor

constructor对应着构造函数,也就是Foo

proto

这是每个对象都有的隐式原型属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用__proto__ 来访问。

new 的过程中,新对象被添加了 __proto__ 并且链接到构造函数的原型上。

Function.proto === Function.prototype

JavaScript核心知识总结(中)_第1张图片
对于对象来说,obj.__proto__.constructor是该对象的构造函数,但是上图标明Function.__proto__=== Function.prototype。从图中可以发现,所有对象都可以通过原型链最终找到Object.prototype,虽然Object.prototype也是一个对象,但是这个对象却不是Object创造的,而是引擎创建的。

引擎创建了 Object.prototype ,然后创建了 Function.prototype ,并且通过 __proto__ 将两者联系了起来。这里也很好的解释了上面的一个问题,为什么 let fun = Function.prototype.bind() 没有 prototype 属性。因为 Function.prototype 是引擎创建出来的对象,引擎认为不需要给这个对象添加 prototype 属性。

所以我们又可以得出一个结论,不是所有函数都是 new Function() 产生的。

有了 Function.prototype 以后才有了 function Function() ,然后其他的构造函数都是 function Function() 生成的。

现在可以来解释 Function.__proto__ === Function.prototype 这个问题了。因为先有的 Function.prototype 以后才有的 function Function() ,所以也就不存在鸡生蛋蛋生鸡的悖论问题了。对于为什么 Function.__proto__会等于 Function.prototype ,个人的理解是:其他所有的构造函数都可以通过原型链找到 Function.prototype ,并且 function Function() 本质也是一个函数,为了不产生混乱就将 function Function()__proto__ 联系到了 Function.prototype 上。

原型小结

  • Object是所有对象的爸爸,所有对象都可以通过__proto__找到它
  • Function是所有函数的爸爸,所有函数都可以通过__proto__找到它
  • Function.prototypeObject.prototype是两个特殊的对象,由引擎创建
  • 除了以上两个对象,其他对象都是通过构造器new创建出来的
  • 函数的 prototype 是一个对象,也就是原型
  • 对象的__proto__ 指向原型, __proto__ 将对象和原型连接起来组成了原型链

instanceof 判断对象的原理是什么?

判断实例对象的__proto__属性与构造函数的prototype是不是用一个引用。如果不是,他会沿着对象的__proto__向上查找的,直到为null

const Person = function(){
     }
const p1 = new Person()
p1 instanceof Person//true

var str = 'hello world'
str instanceof String // true

var str1 = new String('hello world')
str1 instanceof String // true

自己实现一个 instanceof

function instance_of(L, R) {
     
 var O = R.prototype;
 L = L.__proto__;
 while (true) {
     
   if (L === null)
     return false;//最终没找到,返回false
   if (O === L)
     return true;//相等则返回true
   L = L.__proto__;//继续沿着__proto__向上找
 }
}

继承

类的区别

//ES5
function Animal(){
     
    this.name = 'Animal'
}
//ES6
class Animal2{
     
    constructor () {
     
        this.name = 'Animal';
    }
}

原型链继承

function Cat(){
     

}
Cat.prototype=new Animal()
var cat = new Cat()

原理:把子类的prototype(原型对象)直接设置为父类的实例

缺点:因为子类只进行一次原型更改,所以子类的所有实例保存的是同一个父类的值。
当子类对象上进行值修改时,如果是修改的原始类型的值,那么会在实例上新建这样一个值;
但如果是引用类型的话,他就会去修改子类上唯一一个父类实例里面的这个引用类型,这会影响所有子类实例

构造继承

function Cat(){
     
    Animal.call(this)
}

原理: 将子类的this使用父类的构造函数跑一遍

缺点: Parent原型链上的属性和方法并不会被子类继承

实例继承

function Cat(name){
     
    var instance = new Animal()
    instance.name = name || 'cat'
    return instance
}

组合继承

function Cat(){
     
    Animal.call(this)
}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat

特点:

  • 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
  • 既是子类的实例,也是父类的实例
  • 不存在引用属性共享问题
  • 可传参
  • 函数可复用
    缺点:
  • 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

拷贝继承

function Cat(name){
     
    var animal = new Animal();
    for(var p in animal){
     
        Cat.prototype[p] = animal[p];
    }
    Cat.prototype.name = name || 'Tom';
}
var cat = new Cat()

特点:

  • 支持多继承
    缺点:
  • 效率较低,内存占用高(因为要拷贝父类的属性)
    无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)

寄生组合继承

function Cat(){
     
    Animal.call(this)
}
(function(){
     
    var Super = function(){
     }
    Super.prototype = Animal.prototype
    Cat.prototype = new Super()
})()
Cat.prototype.constructor = Cat
var cat = new Cat()

ES5/ES6 的继承除了写法以外还有什么区别?

  • class 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 let、const 声明变量。
  • class 声明内部会启用严格模式。
  • class 的所有方法(包括静态方法和实例方法)都是不可枚举的。
  • class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用。
  • 必须使用 new 调用 class。
  • class 内部无法重写类名。

事件循环

为什么JavaScript是单线程?

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

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

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

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

Event Loop

参考地址:Event Loop 这个循环你晓得么?(附 GIF 详解)-饿了么前端

任务队列的本质

  • 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  • 主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
  • 一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  • 主线程不断重复上面的第三步。

异步任务

  • setTimeOutsetInterval
  • DOM 事件
  • Promise

可分为宏任务和微任务,分类如下:

  • 宏任务:setTimeout,setInterval,setImmediate,I/O(磁盘读写或网络通信),UI交互事件
  • 微任务:process.nextTick,Promise.then

前面我们介绍,事件循环会将其中的异步任务按照执行顺序排列到事件队列中。然而,根据异步事件的不同分类,这个事件实际上会被排列到对应的宏任务队列或者微任务队列当中去。

当执行栈中的任务清空,主线程会先检查微任务队列中是否有任务,如果有,就将微任务队列中的任务依次执行,直到微任务队列为空,之后再检查宏任务队列中是否有任务,如果有,则每次取出第一个宏任务加入到执行栈中,之后再清空执行栈,检查微任务,以此循环… …

JavaScript 实现异步编程的方法?

  • 回调函数
  • 事件监听
  • 发布/订阅
  • Promise 对象
  • Async 函数[ES7]

关于 setTimeOut、setImmediate、process.nextTick()的比较

setTimeout()

将事件插入到了事件队列,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。
当主线程时间执行过长,无法保证回调会在事件指定的时间执行。
浏览器端每次setTimeout会有4ms的延迟,当连续执行多个setTimeout,有可能会阻塞进程,造成性能问题。

setImmediate()

事件插入到事件队列尾部,主线程和事件队列的函数执行完成之后立即执行。和setTimeout(fn,0)的效果差不多。
服务端 node 提供的方法。浏览器端最新的 api 也有类似实现:window.setImmediate,但支持的浏览器很少。

process.nextTick()

插入到事件队列尾部,但在下次事件队列之前会执行。也就是说,它指定的任务总是发生在所有异步任务之前,当前主线程的末尾。
大致流程:当前”执行栈”的尾部–>下一次Event Loop(主线程读取”任务队列”)之前–>触发 process 指定的回调函数。
服务器端 node 提供的办法。用此方法可以用于处于异步延迟的问题。
可以理解为:此次不行,预约下次优先执行。

Promise

Promise 本身是同步的立即执行函数, 当在 executor 中执行 resolve 或者 reject 的时候, 此时是异步操作, 会先执行 then/catch 等,当主栈完成后,才会去调用 resolve/reject 中存放的方法执行,打印 p 的时候,是打印的返回结果,一个 Promise 实例。

async await

Async/Await就是一个自执行的 generate 函数。利用generate 函数的特性把异步的代码写成“同步”的形式。

async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。

正则

特殊字符

  • ^ 匹配输入的开始
  • $ 匹配输入的结束
  • * 匹配0次或多次{0, }
  • + 匹配1次或多次{1, }
  • ?
    • 0次或者1次{0,1}
    • 用于先行断言
    • 如果紧跟在任何量词 *、 +、? 或 {} 的后面,将会使量词变为非贪婪
      • 对 “123abc” 用 /\d+/ 将会返回 “123”,
      • 用 /\d+?/,那么就只会匹配到 “1”。
      • . 匹配除换行符之外的任何单个字符
  • (x) 匹配 ‘x’ 并且记住匹配项
  • (?:x) 匹配 ‘x’ 但是不记住匹配项
  • x(?=y) 配’x’仅仅当’x’后面跟着’y’.这种叫做正向肯定查找。
  • x(?!y) 匹配’x’仅仅当’x’后面不跟着’y’,这个叫做正向否定查找。
  • x|y 匹配‘x’或者‘y’。
  • {n} 重复n次
  • {n, m} 匹配至少n次,最多m次
  • [xyz] 代表 x 或 y 或 z
  • [^xyz] 不是 x 或 y 或 z
  • \d 数字
  • \D 非数字
  • \s 空白字符,包括空格、制表符、换页符和换行符。
  • \S 非空白字符
  • \w 单词字符(字母、数字或者下划线) [A-Za-z0-9_]
  • \W 非单字字符。[^A-Za-z0-9_]
  • \3 表示第三个分组
  • \b 词的边界
    • /\bm/匹配“moon”中得‘m’;
  • \B 非单词边界

正则表达式的方法

  • exec 一个在字符串中执行查找匹配的RegExp方法,它返回一个数组(未匹配到则返回null)
  • test 一个在字符串中执行查找匹配的RegExp方法,返回true或者false
  • match 一个在字符串中执行查找匹配的String方法,它返回一个数组或者在未匹配到时返回null。
  • search 一个在字符串中测试匹配的String方法,它返回匹配到的位置索引,或者在失败时返回-1。
  • replace 一个在字符串中执行查找匹配的String方法,并且使用替换字符串替换掉匹配到的子字符串。
  • split 一个使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中的String方法。

练习

匹配结尾的数字

/\d+$/g

统一空格个数

字符串内如有空格,但是空格的数量可能不一致,通过正则将空格的个数统一变为一个。

let reg = /\s+/g
str.replace(reg, " ");

判断字符串是不是由数字组成

str.test(/^\d+$/);

电话号码正则

  • 区号必填为3-4位的数字
  • 区号之后用“-”与电话号码连接电话号码为7-8位的数字
  • 分机号码为3-4位的数字,非必填,但若填写则以“-”与电话号码相连接
/^\d{3,4}-\d{7,8}(-\d{3,4})?$/

手机号码正则表达式

正则验证手机号,忽略前面的0,支持130-139,150-159。忽略前面0之后判断它是11位的。

/^0*1(3|5)\d{9}$/

使用正则表达式实现删除字符串中的空格

function trim(str) {
     
  let reg = /^\s+|\s+$/g
  return str.replace(reg, '');
}

限制文本框只能输入数字和两位小数点等等

/^\d*\.\d{0,2}$/

只能输入小写的英文字母和小数点,和冒号,正反斜杠(:./)

/^[a-z\.:\/\\]*$/

替换小数点前内容为指定内容

例如:infomarket.php?id=197 替换为 test.php?id=197

var reg = /^[^\.]+/;
var target = '---------';
str = str.replace(reg, target)

只匹配中文的正则表达式

/[\u4E00-\u9FA5\uf900-\ufa2d]/ig

返回字符串的中文字符个数

先去掉非中文字符,再返回length属性。

function cLength(str){
     
  var reg = /[^\u4E00-\u9FA5\uf900-\ufa2d]/g;
  //匹配非中文的正则表达式
  var temp = str.replace(reg,'');
  return temp.length;
}

正则表达式取得匹配IP地址前三段

只要匹配掉最后一段并且替换为空字符串就行了

function getPreThrstr(str) {
     
  let reg = /\.\d{1,3}$/;
  return str.replace(reg,'');
}

匹配
之间的内容

/<ul>[\s\S]+?</ul>/i

用正则表达式获得文件名

c:\images\tupian\006.jpg
可能是直接在盘符根目录下,也可能在好几层目录下,要求替换到只剩文件名。
首先匹配非左右斜线字符0或多个,然后是左右斜线一个或者多个。

function getFileName(str){
     
  var reg = /[^\\\/]*[\\\/]+/g;
  // xxx\ 或是 xxx/
  str = str.replace(reg,'');
  return str;
}

绝对路径变相对路径

“http://23.123.22.12/image/somepic.gif"转换为:”/image/somepic.gif"

var reg = /http:\/\/[^\/]+/;
str = str.replace(reg,"");

用户名正则

用于用户名注册,,用户名只 能用 中文、英文、数字、下划线、4-16个字符。

/^[\u4E00-\u9FA5\uf900-\ufa2d\w]{4,16}$/

匹配英文地址

规则如下:
包含 “点”, “字母”,“空格”,“逗号”,“数字”,但开头和结尾不能是除字母外任何字符。

/^[a-zA-Z][\.a-zA-Z,0-9]*[a-zA-Z]$/

正则匹配价格

开头数字若干位,可能有一个小数点,小数点后面可以有两位数字。

/^\d+(\.\d{2})?$/

身份证号码的匹配

身份证号码可以是15位或者是18位,其中最后一位可以是X。其它全是数字

/^(\d{14}|\d{17})(X|x)$/

单词首字母大写

每单词首字大写,其他小写。如blue idea转换为Blue Idea,BLUE IDEA也转换为Blue Idea

function firstCharUpper(str) {
     
  str = str.toLowerCase();
  let reg = /\b(\w)/g;
  return str.replace(reg, m => m.toUpperCase());
}

正则验证日期格式

yyyy-mm-dd格式
4位数字,横线,1或者2位数字,再横线,最后又是1或者2位数字。

/^\d{4}-\d{1,2}-\d{1,2}$/

去掉文件的后缀名

www.abc.com/dc/fda.asp 变为 www.abc.com/dc/fda

function removeExp(str) {
     
  return str.replace(/\.\w$/,'')
}

验证邮箱的正则表达式

开始必须是一个或者多个单词字符或者是-,加上@,然后又是一个或者多个单词字符或者是-。然后是点“.”和单词字符和-的组合,可以有一个或者
多个组合。

/^[\w-]+@\w+\.\w+$/

正则判断标签是否闭合

例如:

p的内容,同样也是没闭合的标签。

标签可能有两种方式闭合, 或者是

xxx

/<([a-z]+)(\s*\w*?\s*=\s*".+?")*(\s*?>[\s\S]*?(<\/\1>)+|\s*\/>)/i

正则判断是否为数字与字母的混合

不能小于12位,且必须为字母和数字的混

/^(([a-z]+[0-9]+)|([0-9]+[a-z]+))[a-z0-9]*$/i

将阿拉伯数字替换为中文大写形式

function replaceReg(reg,str){
     
  let arr=["零","壹","贰","叁","肆","伍","陆","柒","捌","玖"];
  let reg = /\d/g;
  return str.replace(reg,function(m){
     return arr[m];})
}

去掉标签的所有属性

***td>
变成没有任何属性的
<td>***td>

思路:非捕获匹配属性,捕获匹配标签,使用捕获结果替换掉字符串。正则如下:

/()/

垃圾回收

JavaScript垃圾回收

标记清除(mark and sweep)

  • 这是JavaScript最常见的垃圾回收方式,当变量进入执行环境的时候,比如函数中声明一个变量,垃圾回收器将其标记为“进入环境”,当变量离开环境时(函数执行结束)将其标记为“离开环境”
  • 垃圾回收器会在运行的时候给存储在内存中的所有变量加上标记,然后去掉环境中的变量以及被环境中的变量所引用的变量(闭包),在这些完成后仍存在标记的就是要删除的变量了。

引用计数(reference counting)

  • 在低版本IE中经常会出现内存泄漏,很多时候就是因为采取引用计数的方式进行垃圾回收。引用计数的策略是跟踪记录每个值被使用的次数,当声明了一个变量并将一个引用类型赋值给改变量的时候这个值的引用次数加1,如果该变量的值变成了另外一个,则这个值的引用次数减1,当这个值的引用次数变为0的时候,说明没有变量在引用,这个值没法被访问了,因此可以将其占用的空间回收,这样垃圾回收器会在运行的时候清理掉引用次数为0的值占用的空间

参考链接 内存管理-MDN

V8下的垃圾回收机制

V8实现了准确式GCGC算法采用了分代式垃圾回收机制。因此,V8将内存(堆)分为新生代和老生代

  • 新生代算法

    • 新生代中对象一般存活时间较短,使用 Scavenge GC 算法。
      • 在新生代空间中,内存空间分成两块,分别为From空间和To空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入From空间中,当From空间被沾满时,新生代GC就会启动了。算法会检测From空间中存活的对象并复制到To空间中,如果有失活的对象就会销毁。当复制完成将From空间和To空间互换,这样GC就结束了
  • 老生代算法

    • 老生代中的对象一般存活时间比较长且数量也多,使用了两个算法,分别是标记清除算法(Mark-Sweep)标记压缩算法(Mark-Compact)。对象出现在老生代空间的情况如下
      • 新生代中的对象是否已经经历过一次 Scavenge 算法,如果经历过的话,会将对象从新生代空间移到老生代空间中。
      • To空间的对象占比大小超过25%。在这种情况下为了不影响到内存分配,会将对象从新生代空间移动到老生代空间。
    • 标记清除(Mark-Sweep)
      • Mark-Sweep在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段中,只清楚没有标记的对象。Mark-Sweep最大的问题是在进行一次标记清除回收后,内存空间出现不连续的状态,为了解决这个内存碎片的问题,Mark-Compact被提出。
    • Mark-Compact在整理的过程中,将活着的对象往一端移动,移动完成后,直接清除掉边界外的内存。

内存泄漏

申请的内存没有及时回收掉,则导致内存泄漏

为什么会发生内存泄漏?

虽然前端有垃圾回收机制,但当某块无用的内存,却无法被垃圾回收机制认为是垃圾时,也就发生内存泄漏了

而垃圾回收机制通常是使用标志清除策略,简单说,也就是引用从根节点开始是否可达来判定是否是垃圾

上面是发生内存泄漏的根本原因,直接原因则是,当不同生命周期的两个东西相互通信时,一方生命到期该回收了,却被另一方还持有时,也就发生内存泄漏了

哪些情况会引起内存泄漏

  • 意外的全局变量
  • 遗忘的定时器
  • 使用不当的闭包
  • 遗漏的DOM元素
  • 网络回调

如何监控内存泄漏

内存泄漏是可以分成两类的,一种是比较严重的,泄漏的就一直回收不回来了,另一种严重程度稍微轻点,就是没有及时清理导致的内存泄漏,一段时间后还是可以被清理掉

不管哪一种,利用开发者工具抓到的内存图,应该都会看到一段时间内,内存占用不断的直线式下降,这是因为不断发生 GC,也就是垃圾回收导致的

针对第一种比较严重的,会发现,内存图里即使不断发生 GC 后,所使用的内存总量仍旧在不断增长

另外,内存不足会造成不断 GC,而 GC 时是会阻塞主线程的,所以会影响到页面性能,造成卡顿,所以内存泄漏问题还是需要关注的

举例场景

在某个函数内申请一块内存,然后该函数在短时间内不断被调用

// 点击按钮,就执行一次函数,申请一块内存
startBtn.addEventListener("click", function() {
     
	var a = newArray(100000).fill(1);
	var b = newArray(20000).fill(1);
});

一个页面能够使用的内存是有限的,当内存不足时,就会触发垃圾回收机制去回收没用的内存

而在函数内部使用的变量都是局部变量,函数执行完毕,这块内存就没用可以被回收了

所以当我们短时间内不断调用该函数时,可以发现,函数执行时,发现内存不足,垃圾回收机制工作,回收上一个函数申请的内存,因为上个函数已经执行结束了,内存无用可被回收了

如何分析内存泄漏,找出有问题的代码

借助开发者工具的memory功能

最后

行文至此,感谢阅读,一键三连是对我最大的支持。

你可能感兴趣的:(javascript,node.js,es6,html,css)