JavaScript - 总结

5/16/2022

# JavaScript的编译,上下文context

# 过程

  • 预编译期
    1. 浏览器的js引擎解析js代码
    2. 建立arguments对象(隐藏对象,不可见),函数,参数,变量
    3. 建立作用域链
    4. 确定this指向
  • 执行期
    • 按从上到下的顺序执行代码
  • 函数体的预解析发生在函数被调用之时,被调用时先进行函数体的预编译,然后按顺序进行执行

# 上下文context

  • 指的是一种运行环境
  • context指的是,函数被调用时,看this指向哪个object,这个object就是当前的上下文。

# 执行上下文

  • 概念

    • 用来区分不同的运行环境,需要引出一个概念:执行上下文(Execution Context)
    • 他是一个对象,由js的执行引擎创建
    • 有三个属性:变量对象,作用域链,this指针
  • 上下文栈

    • js执行过程中有一个上下文栈,存放的是不同的上下文对象(不同的js运行环境)。
    • 当前执行代码的context对象总是在栈顶。
  • 变量对象

    • 变量对象的创建过程如下:(变量提升被提出的原因)
      1. 创建arguments对象,其中保存多个属性,属性的key是0,1,2... value即传入参数的实际值。
      2. 找到这个作用域内的所有var和function,作为属性存储在变量对象中,如果是func,则属性名是 函数名,属性值是函数引用。 如果是var,属性名是变量名,属性值是undefined

# js里的引用值,原始值和堆栈的区别

# 引用值

  • 以对象形式存储在堆(heap)中,也就是说,存储在变量中的是一个指向内存的指针。由于引用值的大小 会改变,所以不能存储在栈中,否则会降低变量查询的速度。存放在栈空间的变量的值是该对象存储在堆中的地址,地 址的大小是固定的,所以把他存储在栈中对变量性能无任何负面影响。

# 原始值

  • 存储在栈(stack)中得简单数据段,也就是说它们的值直接存储在变量访问的位置;基本数据类型按值访问 ,因此直接操作保存在变量中的实际值。

#

  • 由操作系统自动分配内存空间,自动释放,存储的是基础变量以及一些对象的引用变量,占 据固定大小的空间。
  • 基础变量的值是存储在栈中,而引用变量存储在栈中的是指向堆中的数组或者对象的地址,这就 是为何修改引用类型总会影响到其他指向这个地址的引用变量。
  • 优点:相比于堆来说存取速度会快,并且栈内存中的数据是可以共享的,例如同时声明了var a = 1 和var b = 1,会先处理a,然后在栈中查找有没有值为1的地址,如果没有就开辟一个值为1的地址,然后a指向这个地址 ,当处理b时,因为值为1的地址已经开辟好了,所以b也会同样指向同一个地址。
  • 缺点:相比于堆来说的缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

#

  • 由操作系统动态分配的内存,大小不定也不会自动释放,一般由程序员分配释放,也可由垃圾回收机 制回收。
  • 堆内存中的对象不会随方法的结束而销毁,就算方法结束了,这个对象也可能会被其他引用变量所引用(参数传递)。 创建对象是为了反复利用(因为对象的创建成本通常较大),这个对象将被保存到运行时数据区(也就是堆内存)。 只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。

# ajax 与 axios

# ajax

  • 原理:Ajax的原理简单来说,实际上就是通过XmlHttpRequest对象来向服务器发异步请求,从服务器获得数据,然后用javascript来操作DOM而更新页面

# axios

  • 介绍:Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
  • 原理:本质上也是对原生xhr的封装,只不过它是promise的实现版本,符合最新的ES规范

# 垃圾回收机制

# 标记清除

  • 就是在变量进入执行环境时标记成“进入环境”,离开时标记成“离开环境”

# 引用计数

  • 就是跟踪一个值的引用次数,当声明一个变量并将一个引用类型赋值给该变量时该值引用次数加1, 当这个变量指向其他一个时该值的引用次数便减1。当该值引用次数为0时,则说明没有办法再访问这个值了, 被视为准备回收的对象。

# 数据类型

# 基本数据类型

  • undefined, number string null boolean ES6新增Symbol, bigint

# 复杂(引用)数据类型

  • object, array,function

# this指向(new,隐式绑定,显式绑定)

  • new一个对象的原理
    • 创建空对象 var obj = newObject()
    • 让Person中的this指向obj,并执行Person这个构造函数。var result = Person.call(obj);
    • 设置原型链,将obj的__proto__指向Person函数对象的prototype成员对象。obj.proto = Person.prototype;
    • 判断result的返回值类型,如果是值类型,返回obj。如果是引用类型,就返回这个引用类型的对象。(因此调用call方法的 时候可能返回了this,也可能没有返回)
      function Person(){
        // let this = {
        //       __proto__: Person.prototype
        // }
        // 创建一个新对象,赋值this,这一步是隐性的
        // 给this指向的对象赋予构造属性
        this.name = name
        this.age = age
        // 如果没有手动返回对象,则默认返回this指向的这个对象,也是隐性
        // return this 
      }
      const person = new Person()
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    • 实现一个new方法
      // 构造器函数
      let Parent = function(name, age) {
        this.name = name
        this.age = age
      }
      Parent.prototype.sayName = function(){
          console.log(this.name)
      }
      
      // 自己定义的new方法
      let newFoo = function(Parent, ...rest) {
          // 以构造器的prototype属性为原型,创建新对象
          let child = Object.create(Parent.prototype)
          // 将this和调用参数传给构造器执行
          let result = Parent.apply(child, rest)
          // 如果构造器没有手动返回对象,则返回第一步的对象
          return typeof result === 'object'? result:child;
      }
      // 创建实例,将构造函数Parent与形参作为参数传入
      const child = newFoo(Parent, 'jack', 26)
      child.sayName()     //'jack'
      
      // 检验,与使用new效果相同
      child.instanceof Parent  //t
      child.hasOwnProperty('name')  //t
      child.hasOwnProperty('age') //t
      child.hasOwnProperty('sayName')  //f
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28

# 关于this指向的问题

  • 要记住几句话:
    1. this 永远指向最后调用它的那个对象
    2. 匿名函数的 this 永远指向 window
    3. 没有挂载在任何对象上的函数,非严格模式下 this 指向 window
  • 有关例子,请移步这里 (opens new window)

# 箭头函数

  • 箭头函数中没有this,若想打印它的话只能沿着作用域链去找。(也就是,箭头函数的this是指向上下文的)
  • 箭头函数不能作为构造函数实例化,也不能被继承。
  • 箭头函数没有arguments

# 深拷贝 浅拷贝

  • 浅拷贝(拷贝指针)
    • for in
    • Object.assign() =>es5新方法
  • 深拷贝(拷贝对象)
    • 有三种方法,第一种JSON.parse 和 JSON.stringify; 第二种,JQ的extends; 第三种,自定义函数
    • 第一种
    var obj = {name:'123'};
    var obj2 = JSON.parse(JSON.stringify(obj))  // 先对象转字符串,然后字符串转对象
    
    1
    2
    • 第二种
    var a = [0,1,[2,3],4];
        b = $.extend(true,[],a);
        a[0] = 1;
        a[2][0] = 7;
        console.log(a);   //  [1,1,[7,3],4];
        console.log(b);   //  [0,1,[2,3],4];
    //$.extend参数:
    //第一个参数是布尔值,是否深复制
    //第二个参数是目标对象,其他对象的成员属性将被附加到该对象上
    //第三个及以后的参数是被合并的对象
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    • 第三种
    // 简单实现版
    function deepCopy(obj){
        //判断是否是简单数据类型,
        if(typeof obj == "object"){
            //复杂数据类型
            var result = obj.constructor == Array ? [] : {};
            for(let i in obj){
                result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
            }
        }else {
            //简单数据类型 直接 == 赋值
            var result = obj;
        }
        return result;
    }
    
    // 考虑循环引用(这个用得多点)
    function deepCopy(target, map=new Map()){
        if(typeof target === 'object'){
            const cloneTarget = Array.isArray(target)? []:{};
            // 这里就是解决循环复用问题的地方,已经存在就直接返回
            if(map.get(target)) return map.get(target);
            map.set(target, cloneTarget);
            for(const key in target){
                cloneTarget[key] = deepCopy(target[key], map)
            }
        } else {
            return target;
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30

# Function的call,apply,bind方法

  • 作用:这三个方法的作用其实都是改变函数内this的指向。实质上,call和apply只是使用了Function的属性,并且调用方法而已。
  • 实际用途:扩充变量的作用域
  • 函数的一些基本知识:
    • 函数本身都是对象
    • 每个函数都有自带两个属性:length和prototype。
    • 函数的arguments对象包含了要传递的参数

# call

  • 使用:call(作用域,枚举参数)

  • 作用:在特定的作用域里(一般是this)执行函数。

  • call与apply不同的是,call必须明确传入每一个参数(枚举)

  • 调用的例子

    function sum(num1, num2){
        return num1 + num2
    }
    function callSum1(num1, num2) {
      return sum.call(this, num1, num2)
    }
    
    alert(callSum1(10, 10))     // 20
    
    1
    2
    3
    4
    5
    6
    7
    8
  • 手写call

// 思路 替换原函数中的this为目标对象,将参数传给原函数,兼容null作目标对象,自动转换为window对象
// 将要改变this指向的方法挂到目标this上执行并返回
Function.prototype.myCall = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('not funciton')
  }
  context = context || window   //兼容传入null的情况
  context.fn = this     //将原函数添加到目标对象属性上
  // es6写法的处理函数的参数,将具有length属性的对象 转成数组
  let arg = [...arguments].slice(1)
  let res = context.fn(...arg)
  delete context.fn     // 去除属性
  return res
}

// test
const me = { name: 'Jack' }
function say() {
  console.log(`My name is ${this.name || 'default'}`);
}
say.myCall(me)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# apply

  • 使用:apply(作用域,参数)

  • 作用:在特定的作用域里(一般是this)调用函数

  • 调用的例子

    // 思路 在context上调用方法,触发this绑定为context, 使用Symbol防止原有属性覆盖
    function sum(num1, num2){
        return num1 + num2
    }
    function callSum1(num1, num2) {
      return sum.apply(this, arguments)
    }
    function callSum2(num1, num2) {
      return sum.apply(this, [num1, num2])
    }
    alert(callSum1(10, 10))     // 20
    alert(callSum2(10, 10))     // 20
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  • 手写apply

// 将要改变this指向的方法挂到目标this上执行并返回
Function.prototype.myApply = function (context = globalThis) {
  const key = Symbol('key')
  context[key] = this
  let res
  if(arguments[1]) {
      res = context[k](...arguments[1])
  } else {
      res = context[key]()
  }
  delete context[key]
  return res
}

// test
const me = { name:'jack' }
function say() {
    console.log('my name is ${this.name || 'default'}')
}
say.myApply(me)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# bind

  • 使用:bind(绑定对象)

  • 作用:把函数的this绑定到绑定对象上

  • 调用的例子

    window.color = 'red'
    var o = { color: 'blue' }
    function sayColor(){
        alert(this.color) 
    }
    var objectSayColor = sayColor().bind(o)
    objectSayColor();       // blue
    
    1
    2
    3
    4
    5
    6
    7
  • 手写bind

Function.prototype.myBind = function (context = globalThis) {
  const fn = this
  const args = Array.from(arguments).slice(1)
  const newFunc = function () {
    const newArgs = args.concat(...arguments)
    if (this instanceof newFunc) {
        // 通过new调用,绑定this为实例对象
        fn.apply(this, newArgs)
    } else {
        // 通过普通函数形式调用,绑定context
        fn.apply(context, newArgs)
    }
  }
  // 支持new调用方式
  newFunc.prototype = object.create(fn.prototype)
  return newFunc
}

// test
const me = { name:'jack' }
const other = { name:'jackson' }
function say(){
    console.log('my name is ${this.name || 'default'}')
}
const mySay = say.bind(me)
mySay()
const otherSay = say.bind(other)
otherSay()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 综合用例

  • 第一道

    var name = 'name1';
    
    var obj = {
     name: 'name2',
     sayName: function(){
      // 返回一个默认全局的函数
      return function(){
       console.log(this.name);
      };
     },
     changeName: function(){
      // 返回一个默认全局的函数
      setTimeout(function(){
       this.name = 'name3';
      // 然后将该函数绑定给this(当前obj对象)
      }.bind(this),0);
     }
    };
    
    // obj.sayName()这个函数,让obj来调用
    obj.sayName().call(obj);
    // 让this(也就是全局对象)来调用
    obj.sayName().apply(this);
    
    obj.changeName();
    setTimeout(function(){
     // 输出更改之后,全局name的值
     console.log(name);
     // 输出更改之后,obj对象中 name的值
     console.log(obj.name);
    },0);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    • 结果: name2 name1 name1 name3
  • 第二道 Function.call.call.call

function f1() {
    console.log(1)
}
function f2() {
  console.log(2)
}
f1.call(f2)     // 1
f1.call.call(f2)    // 2
f1.call.call.call(f2)   // window
1
2
3
4
5
6
7
8
9

# 闭包(概念 用途 手写)

# 概念

  • 闭包是包含了那个局部变量的容器,它被内部函数对象引用。
  • 特点是2个函数相互嵌套,内部函数引用了外部函数的局部变量,执行外部函数。
  • 闭包的本质还是函数

# 作用

  • 延长局部变量的生命周期
  • 使函数外部可以多次间接操作到函数内部的数据。

# 应用

  • 循环遍历监听
  • IIFE定义模块
  • jQuery内部

# 优缺点

  • 优点:延长了变量的生命周期
  • 缺点:无法被回收机制及时回收造成内存泄露,只能手动释放(手动=null)

# 例子

var F = function () {
  var sum = 10;
  return function () {
    console.log(sum)    //注意这里一定不能加this,不然就是全局查找变量,输出undefined
  }
}
var f = new F()
f() //10
1
2
3
4
5
6
7
8

# 高阶函数

###定义

  • 高阶函数是对其它函数进行操作的函数,它接收函数作为参数或者将函数作为返回值输出
  • 说人话就是将一个函数作为返回值输出。
  • 个人感觉,用来封装用的。面向AOP变成(切面,动态织入)

# 条件

  • 函数可以作为参数被传递
  • 函数可以作为返回值输出

# 实例

// 判断数据类型
var isType = function (obj) {
  return function (obj) {
    return Object.prototype.toString.call(obj) === '[object' + type + ']';
  }
}
var isStr = isType('String')    // 封装判断String方法
var Array = isType('Array')     // 封装判断数组方法
console.log(isArray([1,2,3]))   //调用判断数组方法,返回true
1
2
3
4
5
6
7
8
9

# 同步与异步

# 同步

  • 同步:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。如果在函数A返回 的时候,调用者就能够得到预期的结果(即拿到了预期的返回值或者看到了预期的效果),那么这个函数 就是同步的。

# 异步

  • 不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步 任务可以执行了,该任务才会进入主线程执行。如果在函数A返回的时候,调用者还不能马上得到预期 的结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的 。
  • 发起函数和回调函数
  • 异步的执行机制:
    1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)
    2. 主线程之外,还存在一个"任务队列"(task queue),只要异步任务有了运行结果,就在"任务队列"之中放置一个事件
    3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件,那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行
    4. 主线程不断重复上面的第三步

# ES5

# var 变量提升 函数提升

  • var相当于全局变量,他的值可以被内部修改。用var声明的变量/函数在预编译时是有提升的。
  • 变量提升 具体看这一则代码:
    // 源码
    function test(){
        var n = 1
        if(1){
            var n = 2
        }
        console.log(n)  // 输出2
    }
    test()
    if(1){
        var a = 100
    }
    console.log(a)  // 输出100
    
    
    // 预编译
    var n   // 此时n是undefined
    var a   // 此时a是undefined
    function test(){
        n = 1   // n被赋值1
        if(1){
            n = 2   // n被赋值1
        }
        console.log(n)  //输出2
    }
    if(1){
        a = 100     // a被赋值100
    }
    console.log(a)  //输出100
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  • 函数提升。在js里函数的优先级是最高的,如果有同名的函数和变量,函数将优先被提升至作用域顶端。
    var foo = 3
    function test(){
        console.log(foo);   //function foo(){}
        foo=5;
        console.log(foo);   // 5
        function foo(){}
    }
    test();
    console.log(foo)    // 3
1
2
3
4
5
6
7
8
9
  • 提升的意义
    • 提升的意义是人为的,为了解决函数的相互递归问题,也就是两个函数的执行都要调用到对方。

##Object对象及常见方法

# Object.defineProperty

  • Object.defineProperty(obj, prop, desc),作用:直接在一个对象上定义新属性,或者修改一个已经存在的属性。

# ES6

# 块级作用域 let const

# 继承

  • 建立父类
function Person(name) {
    this.name = name;
    this.sum = function () {
        alert(this.name);
    }
}
Person.prototype.age = 10;
1
2
3
4
5
6
7
  • 原型链继承
function Per() {
  this.name = "ker";
}
Per.prototype = new Person();
var per1 = new Per();
console.log(per1.age);
// 判断元素是否在另一个元素的原型链上,per1继承了Person的属性
console.log(per1 instanceof Person);    // true
1
2
3
4
5
6
7
8
  • 让新实例的原型等于父类的实例。

  • 缺点:

    1. 新实例无法向父类构造函数传参
    2. 继承单一
    3. 所有新实例共享父类实例属性。(原型上的属性是共享的,一个属性被修改,另一个也会被修改)
  • 构造函数继承

function Con() {
  Person.call(this, "jer")  //利用call、apply将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行)
  this.age = 12;
}
let con1 = new Con();
console.log(con1.name);
console.log(con1.age);
console.log(con1 instanceof Person);
1
2
3
4
5
6
7
8
  • 特点:只继承了父类构造函数的属性,原型没有继承。可以继承多个构造函数属性(多个call)。 在子类中可向父类传参。

  • 缺点:

    1. 只能继承父类构造函数的属性
    2. 无法实现构造函数复用(每次都要重新调用)
    3. 每个新实例都会有父类构造函数的副本,冗余。
  • 组合继承(常用)(原型+构造)

function SubType(name) {
    Person.call(this, name);
}
SubType.prototype = new Person();
let sub = new SubType("gar");
console.log(sub.name);
console.log(sub.age);
1
2
3
4
5
6
7
  • 重点:结合了传参和复用

  • 特点:1. 可以继承父类原型上的属性,可传参,可复用。 2. 每个新实例引入的构造函数属性私有。

  • 缺点:调用了两次父类构造函数(耗内存),子类构造函数会代替原型上的父类构造函数。

  • 寄生组合继承

// 寄生,content就是F实例的另一种表示法
function content() {
  function F() {};
  F.prototype = obj;
  return new F();
}
let con = content(Person.prototype);    // con实例(F实例)的原型继承了父类函数的原型,上述更像是原型链继承,只不过是继承了原型属性。
// 组合
function Sub() {
    Person.call(this);  // 这个继承了父类构造函数的属性,解决了组合式两次条用构造函数属性的缺点
}
// 重点
Sub.prototype = con;    //继承con实例
con.constructor = Sub;  // 修复实例
var sub1 = new Sub();
console.log(sub1.age)   //10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 重点:解决了组合继承的问题。

  • ES6继承

class Animal {
    constructor(props) {
        this.name = props.name || 'null'
    }
    eat(){
        console.log(this.name + 'Animal name')
    }
}
class Cat extends Animal{
    constructor(props, myAttr) {
        super(props);   //重点 这里一定要有props
        this.type = props.type || 'null'
        this.attr = myAttr  //私有属性
    }
    fly(){
        console.log(this.name + 'cat name')
    }
    myattr(){
        console.log(this.type + '---' + this.attr)
    }
}
let myCat = new Cat({
    name: 'yellow cat',
    type: 'cat'
}, 'cat class')
myCat.eat() //yellow catAnimal name
myCat.fly() //yellow catcat name
myCat.myattr()  //cat---cat class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# Proxy(代理,守卫,拦截)

  • 作用:让函数和其变量私有化
  • 语法
let p = new Proxy(target, handler);
1
  1. target:需要用Proxy包装的目标对象
  2. handler:一个对象,其属性是当执行一个操作时代理的行为函数。
  • 用法
    • 比如这里有一个例子:
const obj = {
    val: 10;
}
obj.val2 = 20;
// 这里用proxy 对obj对象设置get方法和set方法的拦截
const handler = {
    get: function (obj, prop) {
        if(prop == "id"){
            return 6;
        }
        console.log("get");
        return obj[prop];
    },
    set: function(obj, prop, value) {
        if(typeof  value !== "string") {
            throw new Error("Only String values can be stored in this object!");
        } else {
            obj[prop] = value;
        }
    }
}

const initialobj = {
    id: 1,
    name: "Foo bar"
}

const proxiedObject = new Proxy(initialobj, handler);
console.log(proxiedObject.id);
proxiedObject.name = 8;     // 这里会报错,触发拦截
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# Set WeakSet Map WeakMap

  • set和weakSet

    • 一样都是不重复的值的集合。
    • 区别:WeakSet的成员只能是对象,而不能是其它的值。WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该 对象还存在于 WeakSet 之中。
  • map和weakMap

    • 一样都是键值对的集合(hash)。
    • 区别:weakMap只接受对象和null作为键名。weakMap的键名指向的对象不计入垃圾回收机制。也就是说会随着其引用的对象被回收而释放。

# Promise async await

# Promise

  • 解释:是异步编程的一种解决方案,比传统的异步解决方案更合理和强大。

  • 作用:解决回调地狱问题

  • 状态与关系:

    • Promise中约定,当对象创建之后同⼀个Promise对象只能从pending状态变更为fulfilled或rejected中的其中⼀ 种,并且状态⼀旦变更就不会再改变,此时Promise对象的流程执⾏完成并且finally函数执⾏
  • 常用方法

    • all: 只有全部promise成功,才return成功。
    • race: 比赛,看谁最快完成,返回最快完成的状态。
    • allSelected: 接收一个promise数组,如果有非promise项则当做成功,把每个promise结果,集合成数组返回
    • any: 和all相反,只要有一个成功,就返回成功的那个
  • 执行流程

    • new Promise会调用构造器创建一个Promise对象,系统调用匿名回调函数,并且将创建好的resolved和rejected函数,传给函数, 成功后在Promise对象中调用resolve()会把resolve内的参数递给.then(funcion(data))
  • 创建方式

    1. new Promise(fn);
    2. Promise.resolve(fn);
  • promise与事件循环

    • promise在初始化时,传入的函数是同步执行的,然后注册then回调。注册完之后,继续往下执行同步代码。在这之前,then中回调不会执行。 同步代码块执行完毕后,才会在时间循环中检测是否有可用的promise回调,如果有,执行,没有,下一个事件循环。
  • promise与es6

    • es6中提供了一种generator和promise的语法糖,就是async和await。
(async () => {
    let 蔬菜 = await 买菜();
    let 饭菜 = await 做饭(蔬菜);
    let 送饭结果 = await 送饭(饭菜);
    let 通知结果 = await 通知我(送饭结果);
})
1
2
3
4
5
6
  • 手写promise

# 宏任务 微任务 EventLoop

  • 任务队列分两种类型,宏任务先执行,微任务后执行。宏任务会阻塞浏览器的渲染进程,微任务会在通任务结束后立即执行,在渲染之前。

# 为什么JavaScript是单线程?

  • 作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。

# 宏任务(macro Task)

  • IO,setTimeout; setInterval; setImmediate; requestAnimationFrame

# 微任务(micro Task)

  • Promise,prototype,then catch finally; (node环境)process.nextTick; MutationObserver,

# EventLoop 事件循环

  • 为了解决JS异步编程的一种方案。

  • 进程与线程

    • 进程是CPU资源分配的最小单位。
    • 线程是CPU调度的最小单位。
    • 它俩的区别就是进程携带资源,线程不带。
    • 它俩的关系,一个进程可以有多个线程。
  • 浏览器中的EventLoop执行顺序

    • eventLoop会不断循环的去获取任务队列中最老的一个任务(宏任务)推入栈执行,并在当次 循环里依次执行并清空微任务队列里的任务。
    • 执行完微任务队列里的任务,有可能会渲染更新。(浏览器积攒变动以最高60HZ的频率更新视图)
    • 图示
  • 练习的例子:点这里 (opens new window)

# 事件冒泡与事件委托

# 事件冒泡

  • 概念:js所谓的事件冒泡就是子级元素的某个事件被触发,它的上级元素的该事件也被递归执行
  • 例子:假设这里有列表嵌套<ul><li></li></ul>, 我点击li标签,ul标签也会被触发点击事件,这就叫冒泡。
  • 阻止:e.stopPropagation():
  $("ul[data-type='cityPick']").on('click',function(){ 
      alert("父元素ul被点击"); 
  }); 
  $("ul[data-type='cityPick']").on('click','li',function(e){ 
      e.stopPropagation();//阻止冒泡 
      alert("子元素li被点击"); 
  });
1
2
3
4
5
6
7

# 事件委托

  • 概念:事件委托,即利用冒泡的原理,从点击的元素开始,以递归的方式向父元素传播事件。事件目标并不处理事件,而是把事件委托给父元素/祖先元素。
  • 好处:
    1. 对于大量要处理的元素,不用为每个元素都绑定事件,只需要在父元素绑定一次即可,提高性能。
    2. 可以处理动态插入dom中的元素。
  • 实例:(给5个按钮绑定事件)
  <div class="button-group">
      <bottoun type="button" class="btn">提交</bottoun>
      <bottoun type="button" class="btn">提交</bottoun>
      <bottoun type="button" class="btn">提交</bottoun>
      <bottoun type="button" class="btn">提交</bottoun>
      <bottoun type="button" class="btn">提交</bottoun>
  </div>
  <script>
    $(".button-group").on('click','.btn',function(){
      alert($(this).html());
    });
  </script>
1
2
3
4
5
6
7
8
9
10
11
12

# 原型与原型链

  • 原型。所有引用类都有一个__proto__隐式原型对象,属性值是一个普通的对象
  • 所有函数都有一个prototype原型属性,值是一个普通对象
  • 所有引用类型的__proto__属性指向它构造函数的prototype
  • 蓝色的线就是原型链。当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有 找到,则会去它的__proto__隐式原型上查找,即它的构造函数的prototype,如果还没有找到 就会再在构造函数的prototype的__proto__中查找,这样一层一层向上查找就 会形成一个链式 结构,我们称为原型链。

# 函数柯里化(Currying)

# 概念

  • 柯里化是指把有多个参数的函数转成只有一个参数的函数,并返回接受余下参数且返回结果的新函数的技术。
  • 直接看个例子吧:
    function sum(x, y) {
        return x+y
    }
    // 柯里化
    function curryingSum(x) {
        return function (y) {
            return x+y
        }
    }
    sum(1, 2)
    curryingSum(1)(2)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

# 好处

  • 说白了就是在函数里面再封装一层,但是这么做的意义是什么呢?(再提一嘴,bind实现的机制就是currying)
    1. 参数复用
    2. 提前确认
    3. 延迟执行

# currying封装

  • 对于每次使用currying都要对底层函数进行修改,所以做了以下封装:
  • 大概思想就是,通过闭包把初步参数保存下来,然后通过获取剩下的arguments对象进行拼接,最后 执行需要currying的函数。
    function progressCurrying(fn, args) {
        
      var _this = this
      var len = fn.length
      var args = args || []
      
      return function () {
        var _args = Array.prototype.slice.call(arguments)
        Array.prototype.push.apply(args, _args)
        
        // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
        if(_args.length < len) {
          return progressCurrying.call(_this, fn, _args)
        }
        
        // 参数收集完毕,执行fn
        return fn.apply(this, _args)
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

# 通用写法(定长)

function currying(fn, ...args) {
  return fn.length>args.length? (...newArgs) => currying(fn, ...args, ...newArgs) : fn(...args)
}
function sum(a,b) {
  return a+b
}
sum = currying(sum)
sum(1)(2)
sum(1,2)
1
2
3
4
5
6
7
8
9

# 经典面试题,常见!

// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

// 参数多少不定
function add(...args) {
  return args.reduce((a,b) => a+b);
}
function currying(fn) {
  let args = [];
  return function _c(...newArgs){
    if(newArgs.length){
      // 数据合并
      args = [
        ...args,
        ...newArgs
      ]
      return _c
    } else {
      return fn.apply(this, args);
    }
  }
}
let addCurry = currying(add);
console.log(addCurry(1)(2)(3)(4,5)()); // 注意这里多了个括号


// 定长
function add1(a,b,c,d) {
  return [...arguments].reduce((a,b) => a+b);
}
function currying1(fn) {
  let len = fn.length;
  let args = [];
  return function _c(...newArg){
    args = [
      ...args,
      ...newArg
    ];
    if(args.length < len) return _c;    // 返回函数
    else return fn.apply(this, args.slice(0, len))  // 返回结果
  }
}
let useCurry = currying1(add1);
let total = useCurry(1)(2)(3,4);
console.log(total)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 防抖和节流

# 防抖debounce

  • 作用:防抖函数的作用就是控制函数在一定时间内的执行次数。防抖意味着n秒内函数只会被执行一次,如果n秒内 再次被触发,则重新计算延迟时间。

  • 应用场景:

    1. 搜索框输入查询
    2. 表单验证
    3. 按钮提交事件
    4. 浏览器窗口缩放
  • 防抖函数的实现

    • 思路:时间第一次触发,timer是null,调用later(),若immediate是t,那么立即调用 func.apply(this, params); 若immediate是f,那么过wait之后,调用func.apply(this, params)
    • 事件第二次触发,如果timer重置为null,那么流程和第一次触发一样,如果timer不为null,则清空定时器, 重新计时。
    • immediate为true时,表示函数在每个等待时延的开始被调用;为false,表示在时延结束被调用
    // 首次运行时把定时器赋值给一个变量,第二次执行时,如果间隔没超过定时器设定的时间则会清除掉定时器,重新设定定时器,依次反复,当我们停止下
    // 来时,没有执行清除定时器,超过一定时间后触发回调函数。
    function debounce(fn, wait) {
      let timeout = null
      return function () {
        let context = this
        let args = arguments
        if(timeout) clearTimeout(timeout)
        timeout = setTimeout(() => {
          fn.apply(context, args)
        }, wait)
      }
    }
    // test
    const task = () => {console.log('run task')}
    const debounceTask = debounce(task, 1000)
    window.addEventListener('scroll', debounceTask)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

# 函数节流(throttle)

  • 作用:当持续触发事件时,保证一定时间段内只调用一次事件处理函数。
  • 应用场景:
    1. scroll
    2. touchmove
  • 节流实现
function throttle(fn, ms=1000) {
    let canRun = true
    return function (...args) {
      canRun = false
      setTimeout(() => {
          fn.apply(this, args)
          canRun = true
      }, ms)
    }
}
// test
const task = () => {console.log('run task')}
const throttleTask = throttle(task, 1000)
window.addEventListener('scroll', throttleTask)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 防抖与节流的区别

  • 节流是不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而 防抖是只在最后一次事件之后才触发一次函数。比如在页面的无限加载场景下,我们需要用户在滚动页面时 每隔一段时间发一次ajax请求,而不是在用户停止滚动时才请求数据,那么就是节流。

# 重绘与回流/重排

# html加载

  • 页面加载时,浏览器把获取到的HTML代码解析成一个DOM树,DOM树里包含了所有HTML标签,包括display:none 隐藏,还有js动态添加的元素等。
  • 浏览器把所有的样式解析成样式结构体。DOMTree和样式结构体组合后构件Render Tree。

# 重绘(repaint)

  • 重绘就是Render Tree中一些元素需要更新(只影响元素的外观,风格,不影响布局),就称为重绘。
  • 重绘:不涉及任何DOM元素的排版问题的变动为重绘。
  • 触发重绘:color修改,text-align修改,a:hover修改,:hover引起的颜色等。

# 回流(重排)(reflow)

  • Render Tree中的一部分因为元素的规模尺寸,布局,隐藏等改变而需要重新构建,就叫回流。

  • 每个页面至少需要一次回流,就是页面第一次加载的时候。

  • 完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为重绘。

  • 触发重排:

    1. width/height/margin/border/padding修改
    2. 动画,:hover等伪类引起的元素表现变动,display=none等造成页面回流。
    3. appendChild等DOM元素操作
    4. font类style修改。
    5. background修改
    6. scroll页面,不可避免
    7. resize页面(桌面版本的进行浏览器大小缩放
    8. 读取元素属性:offsetLeft\top\height\width, getComputedStyle()等

# 区别与优化

  • 区别

    • 回流必然引起重绘,但重绘不一定会引起回流。
    • 当页面布局和几何属性改变的时候就需要回流。(比如浏览器大小缩放)
    • 回流的代价比重绘大。
  • 优化

    1. DOM的增删行为。如果要多加个子元素,最好是用documentfragment
    2. 几何属性变化(比如border,front-size)。如果要改变多个几何属性,最好将这些属性定义在一个class中,直接修改 class名,这样只引起一次回流。
    3. 元素位置(margin,padding)的变化。做元素位移的动画,不要更改margin之类的属性,使用定位脱离文档流后位置会更好,
    4. 获取元素的偏移量属性。比如scrollTop,offsetTop之类,浏览器为了保证值的正确也会回流得到最新 的值。如果要多次操作,最好取完做个缓存。
    5. 浏览器窗口尺寸改变。 resize也会引起回流。

# 跨域的几种方式

# 什么是跨域

  • 首先,一个域名地址的组成:协议+子域名+主域名+端口号+请求地址
  • js出于安全考虑,不允许跨域调用其他页面对象。跨域就是js同源策略限制。当协议、主域名、子域名、端口号中任意一个不相同,都算作不同的域。
  • 同源策略限制的存在是为了保护用户隐私信息,防止身份伪造等(读取cookie)

# 跨域解决

  • JSONP

    • 原理:利用<script>元素的开放策略,网页可以得到其他源动态产生的JSON数据。
    • 优缺点。优:兼容性好。 缺:仅支持get请求
    • 实现
    <script type="text/javascript">
      function fn(data) {
          alert(data.msg);
      }
    </script>
    <script type="text/javascript" src="http://crrossdomain.com/jsonServerResponse?jsonp=fn"></script>
    
    // ajax
    $ajax({
        url:"http://crrossdomain.com/jsonServerResponse?jsonp=fn",
        dataType: "jsonp",
        type: "get",
        jsonCallback:"fn",  // 自定义传递给服务器的函数名,可省略
        jsonp:"jsonp",  // 把传递函数名的形参callback变成jsonp,可省略
        success: function(data) {
            console.log(data)
        }
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
  • CROS

    • 原理:实现CROS通信的关键是服务器。只要服务器实现了CROS接口,就可以跨源通信。
    • 优缺点。 优:是跨域的根本方法,由浏览器自动完成,支持更强大的HTTP Method。 缺:兼容性IE10以上。
    • 实现:
// 前端设置:
axios.defaults.withCredentials = true   //axios
Vue.http.options.credentials = true     // vue-resource(插件来的,主要应该还是上面的axios配置,这个不装就不用)

// 服务器设置:
header("Access-Control-Allow-Origin:*");
header("Access-Control-Allow-Methods:POST,GET");

// 这里以node为例子
const express = require("express")
const app = express();
app.all("*", function (req, res, next) {
  res.header("Access-Control-Allow-Origin","*");
  res.header("Access-Control-Allow-Headers","content-type");
  res.header("Access-Control-Allow-Methods","DELETE,PUT,POST,GET,OPTIONS");
  if (req.method.toLowerCase() == 'options')
    res.send(200);  //让options尝试请求快速结束
  else
    next();
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 网络安全

# XSS攻击(cross site scripting 跨域脚本攻击)

  • 原理: 将一些隐私数据像 cookie、session 发送给攻击者,将受害者重定向到一个由攻击者控制的网站,在受害者的机器上进行一些恶意操作。
  • 攻击方式:向网站 A 注入 JS代码,然后执行 JS 里的代码,篡改网站A的内容。
  • 防范
    1. HTTPOnly 防止劫取cookie
    2. 输入检查:输入检查一般是检查用户输入的数据中是否包含 <,> 等特殊字符,如果存在,则对特殊字符进行过滤或编码,这种方式也称为 XSS Filter。
    // vuejs 中的 decodingMap
    // 在 vuejs 中,如果输入带 script 标签的内容,会直接过滤掉
    const decodingMap = {
      '&lt;': '<',
      '&gt;': '>',
      '&quot;': '"',
      '&amp;': '&',
      '
      ': '\n'
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

# CSRF/XSRF攻击(Cross Site Request Forgery,跨站请求伪造)

  • 原理:劫持受信任用户向服务器发送非预期请求的攻击方式。
  • 攻击方式:利用受害者浏览器中的cookie发起验证请求,因为是用户浏览器中自带的,所以不会被服务端发现。
  • 防范
    1. 验证码。验证码会强制用户必须与应用进行交互,才能完成最终请求。因为通常情况下,验证码能够很好地遏制 CSRF 攻击。
    2. Referer Check (http请求头中有个referer字段,它记录了该 HTTP 请求的来源地址)。以下是服务端代码
    if (req.headers.referer !== 'http://www.c.com:8002/') {
      res.write('csrf 攻击');
      return;
    }
    
    1
    2
    3
    4
    1. token。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求 中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。

# 浏览器的缓存

# 强制缓存

  • 强缓存只有在资源过期之后,才会向服务器请求资源。
  • 强制缓存判断HTTP首部字段:cache-control,Expires。
    • Expires是服务器时间(绝对时间)。 浏览器检查当前时间,如果还没到失效时间就直接使用缓存文件。缺:服务器时间与客户端时间可能不一致,现今少用,
    • cache-control中的max-age保存相对时间。如果同时存在cache-control 和 Expires,优先cache。 cache-1.jpg

# 协商缓存(对比缓存)

  • 协商缓存是先从缓存中获取对应的数据表示,向服务器发起请求,确认数据是否更新,如果更新,则返回新数据和缓存,反之返回304,告知客户端未更新,可继续使用。
  • 协商缓存通过HTTP的last-modified, Etag字段判断。
    • last-modified:第一次请求资源时服务器返回的字段,表示最后更新的时间。下一次浏览器请求资源时就发送if-modified-since字段。服务器用本地Last-modified时 间与if-modified-since时间比较,如果不一致则认为缓存已过期并返回新资源给浏览器;如果时间一致则发送304状态码,让浏览器继续使用缓存。
    • Etag:资源实体标识(哈希串),当资源内容更新时,Etag会改变。服务器会判断Etag是否发生变化,如果变化则返回新资源,否则304。(有点像webpack实现热更新的原理..) cache-2.jpg

# 从输入URL到页面加载完成,期间发生了什么?

# 完整的过程

  1. 浏览器的地址栏输入URL并按下回车。
  2. 浏览器查找当前URL的DNS缓存记录。
  3. DNS解析URL对应的IP地址。
  4. 根据IP地址建立TCP连接(三次握手)。
  5. 浏览器向服务器发送HTTP请求。
  6. 服务器处理请求,浏览器接收HTTP响应。
  7. 渲染页面,构建DOM树。
  8. 关闭TCP连接(四次挥手)。

# 过程中的几个关键点

  • TCP(传输层协议,面向连接,可靠)

    • DNS解析后,获取到了服务器IP地址,在获取到IP地址后,TCP会开始建立一次链接,就是所谓的“三次握手”:
    1. 第一次握手: 建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;
    2. 第二次握手: 服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
    3. 第三次握手: 客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。 tcp-3.jpg
    • 最后由tcp关闭连接,即四次挥手。(FIN ACK, ACK, FIN ACK, ACK)
      1. 第一次挥手是浏览器发完数据后,发送FIN请求断开连接。
      2. 第二次挥手是服务器发送ACK表示同意,如果在这一次服务器也发送FIN请求断开连接似乎也没有不妥,但考虑到服务器可能 还有数据要发送,所以服务器发送FIN应该放在第三次挥手中。
      3. 这样浏览器需要返回ACK表示同意,也就是第四次挥手。 tcp-4.jpg
  • 浏览器与服务器的交互

    • 浏览器发送http请求
      • 完整的HTTP请求: 起始行、请求头、请求主体 http-1.jpg
    • 浏览器接受响应
      • 服务器在收到浏览器发送的HTTP请求之后,会将收到的HTTP报文封装成HTTP的Request对象,并通过不同的Web服务器进行处理,处理完 的结果以HTTP的Response对象返回,主要包括状态码,响应头,响应报文三个部分。
      • 响应头主要由Cache-Control、 Connection、Date、Pragma等组成。响应体为服务器返回给浏览器的信息,主要由HTML,css,js,图片文件组成。
      • 状态码如下
        1. 1xx:指示信息–表示请求已接收,继续处理。
        2. 2xx:成功–表示请求已被成功接收、理解、接受。
        3. 3xx:重定向–要完成请求必须进行更进一步的操作。
        4. 4xx:客户端错误–请求有语法错误或请求无法实现。
        5. 5xx:服务器端错误–服务器未能实现合法的请求。
  • 页面渲染

    • 如果说响应的内容是HTML文档的话,就需要浏览器进行解析渲染呈现给用户。整个过程涉及两个方面:解析和渲染。在渲染页面之前,需要构建DOM树和CSSOM树。
      1. 构件HTML解析DOM树,并行请求css/img/js
      2. css下载完成,开始构件CSSOM
      3. CSSOM构建结束后,和DOM一起生成Render Tree(渲染树)
      4. 布局(layout),计算出每个节点在屏幕中的位置
      5. 显示(painting):把元素绘制在屏幕上 dom.jpg