前端面试(2)javascript


JavaScript:

  • 原型、继承、原型链、this 指向、设计模式、call, apply, bind,;
  • new 实现、防抖节流、let, var, const 区别、暂时性死区、event、loop;
  • promise 使用及实现、promise 并行执行和顺序执行;
  • async/await 的优缺点;
  • 闭包、垃圾回收和内存泄漏、数组方法、数组乱序, 数组扁平化、事件委托、事件监听、事件模型。

原型,原型链,继承

原型:

当使用构造函数创建一个对象后,在这个对象内部包含一个指针,这个指针指向构造函数的 prototype 属性所对应的值,这个指针被称为对象的为原型。

原型链:

当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它会去它的原型对象里查找属性,这个原型对象又会有一个自己的原型,于是一直这样向上找(因为继承的,所以是向上找),于是就形成了原型链。 原型链的最顶端是 Object.prototype Object.prototype 的原型是 null(null 没有原型),所以原型链的尽头为 null。

在 js 中我们使用构造函数来创建一个新的对象,每个构造函数内部都有一个 prototype 属性值,这个属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。

继承:

继承:利用原型让一个引用类型继承另一个引用类型的属性和方法,并且将原型链作为实现继承的主要方法。
继承一般有 2 种:接口继承和实现继承。接口继承只继承方法签名,实现继承则继承实际的方法。ECMAscript 中只支持实现继承,而且实现继承主要依靠原型链来实现的。

实现继承的主要方法:

1、原型链继承

核心: 将父类的实例作为子类的原型

  • 优点:共享了父类构造函数的方法
  • 缺点:
    • 父类的引用类型值会被所有子类实例共享,但基本类型不会被共享。
    • 子类构建时不能向父类传参
//在构造函数中,一般很少有数组形式的引用属性,大部分情况都是:基本属性 + 方法。
function Parent() {
  this.name = "父亲"; // 实例基本属性 (该属性,强调私有,不共享)
  this.arr = [1, 2]; //  引用类型
}

Parent.prototype.say = function () {
  // -- 将需要复用、共享的方法定义在父类原型上
  console.log("hello");
};

function Child(like) {
  this.like = like;
}

// 这里是关键,创建Parent的实例,并将该实例赋值给Child.prototype
Child.prototype = new Parent(); // 核心

let boy1 = new Child();
let boy2 = new Child();

// 优点:共享了父类构造函数的say方法
console.log(boy1.say(), boy2.say(), boy1.say === boy2.say);
// hello , hello , true

// 缺点1:不能传参数
// 缺点2:多个实例对引用类型的操作会被篡改,但不会影响到基本类型。
console.log(boy1.name, boy2.name, boy1.name === boy2.name); // 父亲,父亲,true

boy1.arr.push(3); // 修改了boy1的arr属性,boy2的arr属性,也会变化,因为两个实例的原型上(Child.prototype)有了父类构造函数的实例属性arr;所以只要修改了boy1.arr,boy2.arr的属性也会变化。  ----  原型上的arr属性是共享的。
console.log(boy2.arr); // [1,2,3]

总结:

  • 共享了父类构造函数的方法,父类的引用类型值会被所有子类实例共享,但基本类型不会被共享。
  • 子类构建时不能向父类传参。

原型链并非十分完美, 它包含如下两个问题:
问题一: 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;
问题二: 在创建子类型(例如创建 Son 的实例)时,不能向父类型(例如 Father)的构造函数中传递参数.

####

2、借用构造函数

  • 核心:创建子类实例时调用父类构造函数(等于是复制父类的实例属性给子类)。
  • 优点:实例之间独立。
    • 子类构建时可以传参数。
    • 子类实例不共享父类构造函数的引用属性。(如 arr 属性)
  • 缺点:
    • 只能继承父类的实例属性和方法,不能继承原型属性/方法(因为没有用到原型)
    • 无法实现复用,每个子类都有父类实例函数的副本,影响性能
function Parent(name) {
    this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
    this.arr = [1]; // (引用类型)
    this.say = function() { // 实例引用属性 (该属性,强调复用,需要共享)
        console.log('hello')
    }
}
function Child(name,like) {
   //继承自Parent
    Parent.call(this,name);
    this.like = like;
}
let boy1 = new Child('小红','apple');
let boy2 = new Child('小明', 'orange ');

// 优点1:可传参
console.log(boy1.name, boy2.name); // 小红, 小明

// 优点2:不共享父类构造函数的引用属性
boy1.arr.push(2);
console.log(boy1.arr,boy2.arr);// [1,2] [1]

// 缺点1:方法不能复用
console.log(boy1.say === boy2.say) // false (说明,boy1和boy2
的say方法是独立,不是共享的)

// 缺点2:不能继承父类原型上的方法
Parent.prototype.walk = function () {   // 在父类的原型对象上定义一个walk方法。
    console.log('我会走路')
}
boy1.walk;  // undefined (说明实例,不能获得父类原型上的方法)

总结:

  • 可以向父类构造函数传参,子类实例不共享父类构造函数的引用属性。(如 arr 属性)
  • 只能继承父类的实例属性和方法,不能继承原型属性/方法(因为没有用到原型)
  • 实例之间是独立的,每个子类都有父类实例函数的副本,无法实现复用,影响性能。

####

3、组合继承(原型链继承和构造函数继承)

  • 核心:用原型链实现对原型属性和方法的继承,用借用构造函数技术来实现实例属性的继承。(原型链继承和构造函数继承的组合,兼具了二者的优点)
  • 优点:
    • 父类的方法可以被复用
    • 父类的引用属性不会被共享
    • 子类构建实例时可以向父类传递参数
  • 缺点:
    • 调用了两次父类的构造函数,第一次给子类的原型添加了父类的 name, arr 属性,第二次又给子类的构造函数添加了父类的 name, arr 属性,从而覆盖了子类原型中的同名参数。这种被覆盖的情况造成了性能上的浪费。
  • 注意:’组合继承’这种方式,要记得修复 Child.prototype.constructor 指向
  • 第一次 Parent.call(this);从父类拷贝一份父类实例属性,作为子类的实例属性,
  • 第二次 Child.prototype = new Parent();创建父类实例作为子类原型,此时这个父类实例就又有了一份实例属性,但这份会被第一次拷贝来的实例属性屏蔽掉,所以多余。
function Parent(name) {
    this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
    this.arr = [1]; // (该属性,强调私有)
}
Parent.prototype.say = function() { // --- 将需要复用、共享的方法定义在父类原型上
    console.log('hello')
}

function Child(name,like) {
      // 核心   第二次
    Parent.call(this,name,like)
    this.like = like;
}
// 核心   第一次
Child.prototype = new Parent()

<!--这里是修复构造函数指向的代码-->

let boy1 = new Child('小红','apple')
let boy2 = new Child('小明','orange')

// 优点1:可以传参数
console.log(boy1.name,boy1.like); // 小红,apple

// 优点2:可复用父类原型上的方法
console.log(boy1.say === boy2.say) // true

// 优点3:不共享父类的引用属性,如arr属性
boy1.arr.push(2)
console.log(boy1.arr,boy2.arr); // [1,2] [1] 可以看出没有共享arr属性。

注意:为啥要修复构造函数的指向?
console.log(boy1.constructor); // Parent 你会发现实例的构造函数居然是Parent。
而实际上,我们希望子类实例的构造函数是Child,所以要记得修复构造函数指向。修复如下
Child.prototype.constructor = Child;

其实 Child.prototype = new Parent()
console.log(Child.prototype.proto === Parten.prototype); // true
在构造函数中创建对象时,这个对象中包含一个指针,这个指针指向构造函数中属性 prototype 的所对应的值。

总结:

  • 父类的方法可以被复用,父类的引用属性不会被共享,子类构建实例时可以向父类传递参数。
  • 调用 两次父类的构造函数,其原型中会存在两份相同的属性/方法,会被第一次拷贝来的实例属性覆盖,这种被覆盖的情况造成了性能上的浪费。
  • ‘组合继承’这种方式,要记得修复 Child.prototype.constructor 指向为 Child,默认指向父类。

####

4、原型式继承

object()对传入其中的对象执行了一次浅复制,将构造函数 F 的原型直接指向传入的对象。

  • 核心:利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。
  • 优点:父类方法可以复用
  • 缺点:
    • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
    • 无法传递参数
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"],
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); //"Shelby,Court,Van,Rob,Barbie"

总结:

  • 父类的方法可以复用,多个实例的引用类型属性指向相同,存在篡改的可能。
  • 无法传递参数

####

5、寄生式继承

  • 核心:在原型式继承的基础上,增强对象,返回构造函数.
  • 优缺点:仅提供一种思路,函数的主要作用是为构造函数新增属性和方法,以增强函数
  • 缺点(同原型式继承):
    • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
    • 无法传递参数
function createAnother(original) {
  var clone = object(original); //通过调用函数创建一个新对象
  clone.sayHi = function () {
    //以某种方式来增强这个对象
    alert("hi");
  };
  return clone; //返回这个对象
}
var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"],
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"

####

6、寄生组合继承(组合 构造函数和寄生模式)

刚才说到组合继承有一个会两次调用父类的构造函数造成浪费的缺点,寄生组合继承就可以解决这个问题。
优缺点:这个例子的高效率体现在它只调用了一次SuperType 构造函数,并且因此避免了在SubType.prototype 上创建不必要的、多余的属性。于此同时,原型链还能保持不变;因此,还能够正常使用instanceofisPrototypeOf()
这是最成熟的方法,也是现在库实现的方法

function inheritPrototype(subType, superType) {
  var prototype = Object.create(superType.prototype); // 创建对象,创建父类原型的一个副本
  prototype.constructor = subType; // 增强对象,弥补因重写原型而失去的默认的constructor 属性
  subType.prototype = prototype; // 指定对象,将新创建的对象赋值给子类的原型
}

// 父类初始化实例属性和原型属性
function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function () {
  alert(this.name);
};

// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}

// 将父类原型指向子类
inheritPrototype(SubType, SuperType);

// 新增子类原型属性
SubType.prototype.sayAge = function () {
  alert(this.age);
};

var instance1 = new SubType("xyc", 23);
var instance2 = new SubType("lxy", 23);

instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance1.colors.push("3"); // ["red", "blue", "green", "3"]

7、ES6 Class extends

核心: ES6 继承的结果和寄生组合继承相似,本质上,ES6 继承是一种语法糖。但是,
寄生组合继承是先创建子类实例 this 对象,然后再对其增强;
而 ES6 先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this。

  • *extends *关键字实现原型继承。
  • constructor是构造方法,this 关键字则代表实例对象。一个类中只能有一个构造函数,则会报错,如果没有显式指定构造方法,则会添加默认的 constructor 方法。
  • super 关键字,必须显示指定是作为函数还是作为对象使用,否则会报错。

具体可以看详细文章:《es6 的类 class》

class Rectangle {

    // constructor
    constructor(height, width) {
        this.height = height;
        this.width = width;
    }

    // Getter
    get area() {
        return this.calcArea()
    }

    // Method
    calcArea() {
        return this.height * this.width;
    }
}

const rectangle = new Rectangle(10, 20);
console.log(rectangle.area);
// 输出 200

-----------------------------------------------------------------
// 继承
class Square extends Rectangle { //extends 关键字实现原型继承

  constructor(length) {
    super(length, length);

    // 如果子类中存在构造函数,则需要在使用“this”之前先调用 super()。
    this.name = 'Square';
  }

  get area() {
    return this.height * this.width;
  }
}

const square = new Square(10);
console.log(square.area);
// 输出 100

ES6 实现继承的具体原理:

class A {}
class B {}
Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
};
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);

总结

1、函数声明和类声明的区别
函数声明会提升,类声明不会。首先需要声明你的类,然后访问它,否则像下面的代码会抛出一个 ReferenceError。

let p = new Rectangle();
// ReferenceError
class Rectangle {}

2、ES5 继承和 ES6 继承的区别

  • 本质上 ES6 继承是 ES5 继承的语法糖
  • ES5 的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到 this 上(Parent.call(this)).
  • ES6 的继承有所不同,实质上是先创建父类的实例对象 this,然后再用子类的构造函数修改 this。因为子类没有自己的 this 对象,所以必须先调用父类的 super()方法,否则新建实例报错。
  • ES6 继承中子类的构造函数的原型链指向父类的构造函数,ES5 中使用的是构造函数复制,没有原型链指向。
  • ES6 子类实例的构建,基于父类实例,ES5 中不是。
  • ES6 Class extends 是 ES5 继承的语法糖
  • JS 的继承除了构造函数继承之外都基于原型链构建的
  • 可以用寄生组合继承实现 ES6 Class extends,但是还是会有细微的差别

##


this 指向,call, apply, bind

this 的指向

this 关键字只与函数的执行环境有关,而与声明环境没有关系.
this 关键字虽然会根据环境变化,但是它始终指向的是调用当前函数的那个对象

//常见的三种调用函数的方式
func(p1, p2);
obj.child.method(p1, p2);
func.call(context, p1, p2); // 先不讲 apply

从看到这篇文章起,你一定要记住,第三种调用形式,才是正常调用形式:

func.call(context, p1, p2)

其他两种都是语法糖,可以等价地变为 call 形式:

func(p1, p2); //等价于
func.call(undefined, p1, p2);

obj.child.method(p1, p2); //等价于
obj.child.method.call(obj.child, p1, p2);

至此我们的函数调用只有一种形式:

func.call(context, p1, p2)

这样,this 就好解释了 this,就是上面代码中的 context。就这么简单。

var obj = {
  foo: function () {
    console.log(this);
  },
};
var bar = obj.foo;
obj.foo(); // obj => obj.foo.call(obj)
bar(); // window    =>bar.call()转换为 bar.call()由于没有传 context,所以 this 就是 undefined,最后浏览器给你一个默认的 this —— window 对象

var x = 10;
var obj = {
  x: 20,
  f: function () {
    console.log(this.x); // 20。典型的隐性绑定,这里 f 的this指向上下文 obj ,即输出 20
    var foo = function () {
      console.log(this.x);
    };
    foo(); // 10 ,非箭头函数,普通函数谁调用它,this 就指向谁。
    foo.call(this); //20改变 。使用call改变 this 的指向。
  },
};
obj.f();

this 是你 call 一个函数时传的 context,由于你从来不用 call 形式的函数调用,所以你一直不知道。
按理说打印出来的 this 应该就是 undefined 了吧,但是浏览器里有一条规则:

如果你传的 context 是 null 或 undefined,那么 window 对象就是默认的 context(严格模式下默认 context 是 undefined)

this 指向什么,完全取决于什么地方以什么方式调用,而不是创建时。

  • 如果函数被 new 修饰
    • this 绑定的是新创建的对象,例:var bar = new foo(); 函数 foo 中的 this 就是一个叫 foo 的新创建的对象 , 然后将这个对象赋给 bar , 这样的绑定方式叫 new 绑定 .
  • 如果函数是使用 call,apply,bind 来调用的
    • this 绑定的是 call,apply,bind 的第一个参数.例: foo.call(obj); , foo 中的 this 就是 obj , 这样的绑定方式叫 显性绑定 .
  • 如果函数是在某个 上下文对象 下被调用
    • this 绑定的是那个上下文对象,例 : var obj = { foo : foo }; obj.foo(); foo 中的 this 就是 obj . 这样的绑定方式叫 隐性绑定 .
  • 如果都不是,即使用默认绑定
    • 例:function foo(){…} foo() ,foo 中的 this 就是 window.(严格模式下默认绑定到 undefined).
    • 这样的绑定方式叫 默认绑定。

apply, call, bind 改变 this 的指向

  • ** apply(context, 实参) ,2 个参数,实参为数组, 自动执行函数**
  • ** call(context, 实参), 无数个参数,实参为单个参数传入,自动执行函数**
  • ** bind(context, 实参), 无数个参数,实参为单个参数传入 ,不会自动执行函数, 需要手动执行,并返回新的函数**
function show(...arg) {
  //...赋值,表示多个参数
  console.log(arg);
  console.log(this.name);
}
var person = {
  name: "klivitam",
  age: 24,
};
show.call(person, "男", "爱好唱歌", "宅男"); //参数单个传入
// ["男", "爱好唱歌", "宅男"]
// klivitam
show.apply(person, ["男", "爱好唱歌", "宅男"]); //参数以数组的方式传入
// ["男", "爱好唱歌", "宅男"]
// klivitam

具体可参考此篇文章:[bind,call, apply 的指向](https://www.yuque.com/docs/share/a7793549-fefe-4e1f-9d8d-8b49b7d33072?# 《改变 this 的指向(弄懂 this 的指向)》)


节流和防抖

防抖:触发需要重新等待一段时间后再触发(搜索框输入,提交按钮)

function debounce(func, time) {
  let timer = null;
  return () => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this, arguments);
    }, time);
  };
}

节流:等待一段时间后 才会重新触发。(上拉加载更多)

function throtte(func, time) {
  let activeTime = 0;
  return () => {
    const current = Date.now();
    if (current - activeTime > time) {
      func.apply(this, arguments);
      activeTime = Date.now();
    }
  };
}

##


设计模式


懒加载和预加载

懒加载:

原理:先将 img 标签中的 src 链接设为同一张图片(默认占位图片),将其真正的图片地址存储再 img 标签的自定义属性中(比如 data-src)。当 js 监听到该图片元素进入可视窗口时,即将自定义属性中的地址存储到 src 属性中,达到懒加载的效果。

js 监听到该图片元素进入可视窗口时:当前元素距离顶部距离< 当前滚动距离+当前的窗口的高度

function isVisible($node) {
  var winH = $(window).height(), //当前窗口的高度
    scrollTop = $(window).scrollTop(), //当前窗口的滚动距离
    offSetTop = $(window).offSet().top; //当前窗口距离顶部距离
  if (offSetTop < winH + scrollTop) {
    return true;
  } else {
    return false;
  }
}

预加载:

提前加载,当用户需要查看时可直接从本地缓存中渲染。


跨域

为什么会出现跨域问题

出于浏览器的同源策略限制。同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说 Web 是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略会阻止一个域的 javascript 脚本和另外一个域的内容进行交互。

同源

所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)。

什么是跨域

当一个请求 url 的协议、域名、端口三者之间任意一个与当前页面 url 不同即为跨域。

跨域的解决方案:

image.png

JSONP

  • 原理JSONP 主要就是利用了 script 标签 src 属性没有跨域限制来完成跨域。(还有 img 标签也可以没有跨域限制)
  • 限制:参数只能通过 url 传入,仅能支持 get 请求。
  • 实现步骤:
    • Step1: 创建 callback 方法
    • Step2: 插入 script 标签
    • Step3: 后台接受到请求,解析前端传过去的 callback 方法,返回该方法的调用,并且数据作为参数传入该方法
    • Step4: 前端执行服务端返回的方法调用。

websocket

  • 原理:使用 ws://(非加密)和 wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器端支持,无需任何配置就支持跨域。
  • 限制
  • 使用
const ws = new WebSocket("ws://127.0.0.1:3000");
ws.onopen = function () {
  // 连接成功建立
};

ws.onmessage = function (event) {
  // 处理数据
};

ws.onerror = function () {
  // 发生错误时触发,连接中断
};

ws.onclose = function () {
  // 连接关闭时触发
};

postMessage

  • 原理:HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多可以跨域操作的 window 属性之。postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。

  • 限制:对于目标域限制不严格导致的,*大多数开发人员由于对于 postmessage 防范中 targetOrigin 参数默认为\ **,因此只要包含了该方法页面,构造利用代码,就能够获取到敏感信息。

  • 使用场景:

    • 页面和其打开的新窗口的数据传递
    • 多窗口之间消息传递
    • 页面与嵌套的 iframe 消息传递
  • 使用:

a、localhost:3000 发送消息

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>跨域消息传递</title>
  </head>
  <body>
    <div>
      <input type="text" value="hello,missfresh" /><button id="send">发送消息</button>
    </div>
    <iframe
      src="http://localhost:8080/#/reciveMessage" width="500" height="500"id="receiver"
    ></iframe>
  </body>
</html>
<script>
  window.onload = function() {
    var receiver = document.getElementById("receiver").contentWindow;
    var btn = document.getElementById("send");

    btn.addEventListener("click", function(e) {
      e.preventDefault();
      var val = document.getElementById("text").value;
      receiver.postMessage(val + "!", "*");
    });
  };
</script>

b、localhost:8080 接收消息

window.onload = function () {
  var messageEle = document.getElementById("message");
  window.addEventListener("message", function (e) {
    console.log(e.data);
    messageEle.innerHTML = "从" + e.origin + "收到消息: " + e.data;
  });
};

vue 的 proxyTable 跨域

原理:在本地运行 npm run dev 等命令时实际上是用 node 运行了一个服务器 A,因此 proxyTable 实际上是将请求发给服务器 A,再由服务器 A 转发给后台服务器,做了一层代理,所以不会出现跨域问题。(浏览器是禁止跨域的,但是服务端不禁止。)简单来说就是使用 node 作为代理转发请求。
限制
使用:

proxyTable: {
    '/api/*': {
        target: 'http://127.0.0.1:8088', //实际请求的服务器地址
        changeOrigin: true,
        pathRewrite: {
            '^/api': ''
        },
        headers:{    //这边可以堆headers进行设置

            }
    }
}

http 缓存和浏览器缓存:

image.png

什么是 HTTP 缓存 ?

http 缓存指的是: 当客户端向服务器请求资源时,会先抵达浏览器缓存,如果浏览器有“要请求资源”的副本,就可以直接从浏览器缓存中提取而不是从原始服务器中提取这个资源。
常见的 http 缓存只能缓存 get 请求响应的资源,对于其他类型的响应则无能为力,所以后续说的请求缓存都是指 GET 请求。

http 缓存都是从第二次请求开始的。第一次请求资源时,服务器返回资源,并在 respone header 头中回传资源的缓存参数;第二次请求时,浏览器判断这些请求参数,命中强缓存就直接 200,否则就把请求参数加到 request header 头中传给服务器,看是否命中协商缓存,命中则返回 304,否则服务器会返回新的资源。
1、http 缓存的分类:
根据是否需要重新向服务器发起请求来分类,可分为(强制缓存,协商缓存) 根据是否可以被单个或者多个用户使用来分类,可分为(私有缓存,共享缓存) 强制缓存如果生效,不需要再和服务器发生交互,而协商缓存不管是否生效,都需要与服务端发生交互。下面是强制缓存和协商缓存的一些对比:

image

1.1、强制缓存
强制缓存在缓存数据未失效的情况下(即 Cache-Control 的 max-age 没有过期或者 Expires 的缓存时间没有过期),那么就会直接使用浏览器的缓存数据,不会再向服务器发送任何请求。强制缓存生效时,http 状态码为 200。这种方式页面的加载速度是最快的,性能也是很好的,但是在这期间,如果服务器端的资源修改了,页面上是拿不到的,因为它不会再向服务器发请求了。这种情况就是我们在开发种经常遇到的,比如你修改了页面上的某个样式,在页面上刷新了但没有生效,因为走的是强缓存,所以 Ctrl + F5 一顿操作之后就好了。 跟强制缓存相关的 header 头属性有(Pragma/Cache-Control/Expires)

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。当 web 缓存发现请求的资源已经被存储,它会拦截请求,返回该资源的拷贝,而不会去源服务器重新下载。这样带来的好处有:缓解服务器端压力,提升性能(获取资源的耗时更短了)。

HTTP 缓存的作用

我们都知道浏览器是基于 HTTP 协议和服务端进行通信的,一个网站一旦同时请求过多或者请求过大就容易造成页面渲染时长过长等性能问题,而且并非所有资源都需要实时更新的,将长久或一段时间内的资源进行缓存,能很大的缓解服务器压力和提升网站性能。
毫不夸张的说,HTTP 缓存是达到高性能的重要组成部分。

注意:缓存需要合理配置,因为并不是所有资源都是永久不变的:重要的是对一个资源的缓存应截止到其下一次发生改变(即不能缓存过期的资源)。

HTTP 头缓存相关字段及优先级

强缓存:Expires: Date/Cache-Control:max-age=N
协商缓存:Last-Modified:DateEtag:String
通过查询标准我们知道 Cache-Control 和 Etag 属于 HTTP1.1 版本,Expires 和 Last-Modified 属于 HTTP1.0 版本,所以得出以下优先级:
强缓存:Cache-Control > Expires
协商缓存:Etag > Last-Modified

注意:
Expires 存在的缺陷是返回的到期时间是服务器端的时间,可能与客户端的时间有较大的时间差,所以在 HTTP1.1 版开始使用 Cache-Control: max-age=秒替代
Last-Modified 的缺陷:由于只能精确到秒,如果一个文件在 1 秒内多次修改,这时客户端无法识别,因此 HTTP1.1 版本使用 Etag 标识资源内容是否有变更来确认资源是否需要更新,相对来说更加精确

强缓存与协商缓存

强缓存:资源一旦被强缓存,在缓存时间内,浏览器发起二次请求时会直接读取本地缓存,不与服务器进行通讯。 强缓存时间过期的,浏览器会判断资源的响应头是否有 Last-Modified 和 Etag 字段,有的话执行协商缓存策略
协商缓存:如果响应头中的包括有 Etag 和 Last-Modified 字段,则客户端将 If-None-Match:Etag 的值和 If-Modified-Since:Last-Modified 的值添加到请求头发送给服务器,由源服务器校验,如果资源未过期则返回 304 状态码,浏览器直接使用缓存,否则返回 200OK 状态码和新资源。
当两种情况都存在时,强缓存优先级要高于协商缓存

Chrome 浏览器的三种缓存策略

_选择 Chrome 是因为它是现在最流行的网页调试工具也是最多人用的浏览器。_Chrome 浏览器返回缓存 http 状态码总共有以下三个 1、200 from memory cache客户端不与服务器通讯,直接从内存中读取缓存。此时的数据时缓存到内存中的,当关闭浏览器后,数据自然就被当垃圾回收清空。
2、200 from disk cache客户端不与服务器通讯,直接从磁盘中读取缓存,因为数据存在磁盘中,就算关闭浏览器数据还是存在,下次打开只要数据不过期就可以直接读取。
3、304 Not Modified客户端与服务器通讯,服务器验证资源是否需要更新,如果不需要更新服务器返回 304 状态码,然后客户端直接从缓存中读取数据

注意:经过测试,我发现 Safari 和 Firefox 都有三种缓存策略,IE 和其他浏览器大家可以各自测试一下

浏览器三种缓存示例图

Chrome 和 Safari 似乎没有办法在浏览器中直接查看缓存情况,因此只能实践中查看。 Chrome 示例图: 状态码:200 OK
状态码:200 from memeory cache状态码:200 from disk cache状态码:304 Not Modified))
Safari 示例图: 响应头:
状态码:200 OK状态码:200 内存状态码:200 磁盘
Firefox:在 url 上输入 about:cache 可以看到对应的缓存情况,大家可以试一下
从截图中可以看到 Firefox 也分为内存缓存和磁盘缓存,304Not Modified 自然也是有的。资源被强缓存后状态码依然是 200 OK,不过会在传输列下显示已缓存,但是无法看出是内存缓存还是磁盘缓存。Firefox 的 304 与 Chrome 和 Safari 差别不大。

三种缓存策略实际执行的条件

我在网上看到有人写文章说 js、图片和字体保存在内存中而 css 则保存在磁盘,很明显,只要自己稍微测试一下就知道这种说法是站不住脚的,那么这三种情况究竟是怎样的呢?
经过简单的测试以后我发现这三种策略并不复杂,默认配置情况下,Chrome 第一次请求资源后,如果资源的响应头有 Cache-Control 或者 Expires 且有效期大于现在,则加载数据后将强缓存资源到内存和磁盘。
刷新页面,Chrome 发起整个页面的二次请求后,通过开发者工具可以看到强缓存资源都会从内存进行读取,这就是200 from memory cache的情况。
示例:
这时关闭浏览器后,重新打开浏览器并打开关闭前的页面,通过开发者工具可以看到之前强缓存资源都会从磁盘中读取,这是因为关闭了浏览器后系统回收了内存资源,因此内存没有了之前的强缓存资源,需要从磁盘中读取,这就是200 from disk cache的情况。
示例:
如果这时使用 ctrl + f5 强刷页面则会发现全部资源都是 200 OK 状态要从服务器中获取新数据。
304 Not Modified 的情况则完全不同,如果资源的响应头是 Last-Modified 或 Etag,第一次请求资源后缓存到本地磁盘,但第二次也必须发起请求到服务器进行查询该资源是否过期或被修改过,当服务器验证资源没有过期后才会返回 304 Not Modified 状态码,同时响应体为空,这样可以节省流量并提高响应速度,客户端接收到 304 状态码后从本地读取数据,因此 304 比 200 from cache 响应速度要慢,但比 200 OK 快得多。

Chrome 浏览器缓存机制流程图

伪类和伪元素

伪类:其核心就是用来选择那些不能够被普通选择器选择的文档之外的元素,比如:hover,:active。
伪元素:其核心就是需要创建通常不存在于文档中的元素,比如::before。
区别:
针对作用选择器的效果,伪类需要添加类来达到效果,而伪元素需要增加元素,所以一个叫伪类,另外一个叫伪元素。

  • 伪类和伪元素都是用来表示文档树以外的”元素”。
  • 伪类和伪元素分别用单冒号:和双冒号::来表示。
  • 伪类和伪元素的区别,最关键的点在于如果没有伪元素(或伪类),是否需要添加元素才能达到目的,如果是则是伪元素,反之则是伪类。

浏览器渲染的过程:

  1. 处理 HTML 标记并构建 DOM 树。
  2. 处理 CSS 标记并构建 CSSOM 树。
  3. 将 DOM 与 CSSOM 合并成一个渲染树。
  4. 根据渲染树来布局,以计算每个节点的几何信息。
  5. 将各个节点绘制到屏幕上。

需要明白,这五个步骤并不一定一次性顺序完成。如果 DOM 或 CSSOM 被修改,以上过程需要重复执行,这样才能计算出哪些像素需要在屏幕上进行重新渲染。实际页面中,CSS 与 JavaScript 往往会多次修改 DOM 和 CSSOM,下面就来看看它们的影响方式。

var、let、const 区别

var 申明的是全局作用域,全局变量
let 声明的变量拥有块级作用域,局部变量
const 就是用来声明常量,块级作用域,局部变量

var 能够进行变量提升,是全局变量,能够重新申明你变量,let ,const 都是块级作用域,不允许重复申明,重复申明会报错,没有变量提升,const 是常量,一旦申明就不能被修改,并且需要有初始值。

声明方式 变量提升 暂时性死区 重复声明 初始值 作用域
var 允许 不存在 允许 不需要 除块级
let 不允许 存在 不允许 不需要 块级
const 不允许 存在 不允许 需要 块级
  • 变量提升:变量可在声明之前使用。
console.log(a); //正常运行,控制台输出 undefined
var a = 1;

console.log(b); //报错,Uncaught ReferenceError: b is not defined
let b = 1;

console.log(c); //报错,Uncaught ReferenceError: c is not defined
const c = 1;

var 命令经常会发生变量提升现象,按照一般逻辑,变量应该在声明之后使用才对。为了纠正这个现象,ES6 规定 letconst 命令不发生变量提升,使用 letconst 命令声明变量之前,该变量是不可用的。主要是为了减少运行时错误,防止变量声明前就使用这个变量,从而导致意料之外的行为。

  • 暂时性死区(形成封闭式死区)
    • 概述:如果在代码块中存在 letconst 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
var tmp = 123;

if (true) {
  tmp = "abc"; //报错,Uncaught ReferenceError: tmp is not defined
  let tmp;
}

这段代码的原意是在 if 内定义外部的 tmp 为 ‘abc’。
但现实是,存在全局变量 tmp,但是块级作用域内 let 又声明了一个 tmp变量,导致后者被绑定在这个块级作用域中,所以在 let 声明变量前,对 tmp 赋值就报错了。

  • 重复声明:指在相同作用域内,重复声明同一个变量。
function func() {
  let a = 10;
  const PI = 3.1415;

  var a = 1; // 报错,Uncaught SyntaxError: Identifier 'a' has already been declared
  var PI = 3; // 报错,Uncaught SyntaxError: Identifier 'PI' has already been declared
}
// 当调用func()时报错,Uncaught SyntaxError: Identifier 'a' has already been declared
function func() {
  let a = 10;
  const PI = 3.1415;

  let a = 1; // 报错,Uncaught SyntaxError: Identifier 'a' has already been declared
  const PI = 3; // 报错,Uncaught SyntaxError: Identifier 'PI' has already been declared
}

letconst 命令声明的变量不允许重复声明,但是 var 可以重复申明

var i = 10;
for (var i = 0; i < 5; i++) {
  console.log(i);
}
console.log(i); // 输出 5

var i = 10;
for (let i = 0; i < 5; i++) {
  console.log(i);
}
console.log(i); // 输出 10

因为 var 命令没有块级作用域,所以 for 循环括号内的变量 i 会覆盖外层 i,而且 var 允许重复声明,所以这段代码中 i 被声明了两次,i 的最终结果就被 for 循环的 i 给覆盖了。

  • 初始值
    • 由于 const 声明的是只读的常量,一旦声明,就必须立即初始化,声明之后值不能改变。
//一旦被申明,就不能再改变
const PI = 3.1415;
PI = 3;
console.log(PI); // 报错,Uncaught TypeError: Assignment to constant variable.

//必须要有初始值
const PI;
PI=3.1415;
console.log(PI);//Uncaught SyntaxError: Missing initializer in const declaration
  • 作用域

在 ES5 中只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。
第一种场景,内层变量可能会覆盖外层变量:

var tmp = new Date(); //处于全局作用域

function f() {
  console.log(tmp); //处于函数作用域
  if (false) {
    var tmp = "hello world";
  }
}

f(); // undefined

function 内部的 2 个 tmp 变量处在同一函数作用域,由于变量提升,导致函数作用域中的 tmp 覆盖全局作用域中的 tmp,所以,f()输出结果为 undefined。

第二种场景,用来计数的循环变量泄露为全局变量(前面在重复声明中提到的):

//上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
var i = 10;

for (var i = 0; i < 5; i++) {
  console.log(i);
}
console.log(i); // 输出 5

promise

定义:

promise 是异步编程的一种解决方法,从语法上说 promise 是一个对象,从它可以获取异步操作的消息。promise 的出现是为了解决回调地狱

缺点:

参数传递太麻烦,Promise 解决了回调地狱的问题,但是如果遇到复杂的业务,代码里面会包含大量的 then 函数,使得代码依然不是太容易阅读。

Promise 状态和值

Promise 对象存在以下三种状态:Pending(进行中)、Resolved(已成功)、Rejected(已失败)。状态只能由 Pending 变为 Resolved 或由 Pending 变为 Rejected ,且状态改变之后不会在发生变化,会一直保持这个状态。

Promise….then…catch…finally

//Promise的状态值一旦改变就不会再改变, 如果二者都存在,即为只执行在前面的项
new Promise((resolve, reject) => {
  resolve(res); //成功时执行
  reject(err); //失败时执行(此时不执行)
})
  .then((res) => {
    //接收resolve()中的参数
    console.log("成功" + res);
  })
  .catch((err) => {
    //接收reject()中的参数
    console.log("失败" + err);
  })
  .finally(() => {});

Promise 的静态方法

//成功
var p1 = Promise.resolve(value);
// 等价于
var p1 = new Promise((resolve) => resolve(value));

//失败
var p2 = Promise.reject("err");
// 等同于
var p2 = new Promise((resolve, reject) => reject("err"));

###

###

Promise.race

类方法,多个 Promise 任务同时执行,返回最先执行结束的 Promise 任务的结果,不管这个 Promise 结果是成功还是失败。

Promise.race([
  new Promise((resolve, reject) => {
    setTimeout(resolve, 500, "one");
  }),
  new Promise((resolve, reject) => {
    setTimeout(reject, 100, "two");
  }),
  new Promise((resolve, reject) => {
    setTimeout(reject, 300, "three");
  }),
])
  .then((m) => {
    console.log(m);
  })
  .catch((l) => {
    console.log(l);
  });

//返回值为two

###

Promise.all

类方法,多个 Promise 任务同时执行。
如果全部成功执行,则以数组的方式返回所有 Promise 任务的执行结果。 如果有一个 Promise 任务 rejected,则只返回 rejected 任务的结果。

Promise.all([Promise.resolve("成功了"), Promise.reject("失败了"), 3])
  .then((m) => {
    console.log(m);
  })
  .catch((l) => {
    console.log(l); //失败了
  });

##


async/await 的优缺点

Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它。复杂逻辑中,我们就能发现async/await确实比 then 链有优势。
async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。可以说 async 是 Generator 函数的语法糖,并对 Generator 函数进行了改进。

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

async 函数对 Generator 函数的改进,体现在以下四点:

  1. 内置执行器。Generator 函数的执行必须依靠执行器,而 async 函数自带执行器,无需手动执行 next() 方法。
  2. 更好的语义。async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
  3. 更广的适用性。co 模块约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
  4. 返回值是 Promise。async 函数返回值是 Promise 对象,比 Generator 函数返回的 Iterator 对象方便,可以直接使用 then() 方法进行调用。

async/await 执行顺序

通过上面的分析,我们知道async隐式返回 Promise 作为结果的函数,那么可以简单理解为,await 后面的函数执行完毕时,await 会产生一个微任务(Promise.then 是微任务)。但是我们要注意这个微任务产生的时机,它是执行完 await 之后,直接跳出 async 函数,执行其他代码(此处就是协程的运作,A 暂停执行,控制权交给 B)。其他代码执行完毕后,再回到 async 函数去执行剩下的代码,然后把 await 后面的代码注册到微任务队列当中。

console.log("script start");
async function async1() {
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2 end");
}
async1();
setTimeout(function () {
  console.log("setTimeout");
}, 0);
new Promise((resolve) => {
  console.log("Promise");
  resolve();
})
  .then(function () {
    console.log("promise1");
  })
  .then(function () {
    console.log("promise2");
  });
console.log("script end");
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout

分析这段代码:

  • 执行代码,输出script start
  • 执行 async1(),会调用 async2(),然后输出async2 end,此时将会保留 async1 函数的上下文,然后跳出 async1 函数。
  • 遇到 setTimeout,产生一个宏任务
  • 执行 Promise,输出Promise。遇到 then,产生第一个微任务
  • 继续执行代码,输出script end
  • 代码逻辑执行完毕(当前宏任务执行完毕),开始执行当前宏任务产生的微任务队列,输出promise1,该微任务遇到 then,产生一个新的微任务
  • 执行产生的微任务,输出promise2,当前微任务队列执行完毕。执行权回到 async1
  • 执行 await,实际上会产生一个 promise 返回,即
let promise_ = new Promise((resolve,reject){ resolve(undefined)})

执行完成,执行 await 后面的语句,输出async1 end

  • 最后,执行下一个宏任务,即执行 setTimeout,输出setTimeout

使用:

function takeLongTime(n) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(n), n);
  });
}
function step1(n) {
  console.log(`step1 with ${n}`);
  return takeLongTime(n);
}

function step2(m, n) {
  console.log(`step2 with ${m} and ${n}`);
  return takeLongTime(m + n);
}

function step3(k, m, n) {
  console.log(`step3 with ${k}, ${m} and ${n}`);
  return takeLongTime(k + m + n);
}
  • promise 版本
function doIt() {
  console.time("doIt");
  const time1 = 300;
  step1(time1)
    .then((time2) => {
      return step2(time1, time2).then((time3) => [time1, time2, time3]);
    })
    .then((times) => {
      const [time1, time2, time3] = times;
      return step3(time1, time2, time3);
    })
    .then((result) => {
      console.log(`result is ${result}`);
      console.timeEnd("doIt");
    });
}
doIt();
  • async/await 版本
async function doIt() {
  console.time("doIt");
  const time1 = 300;
  const time2 = await step1(time1);
  const time3 = await step2(time1, time2);
  const result = await step3(time1, time2, time3);
  console.log(`result is ${result}`);
  console.timeEnd("doIt");
}
doIt();

Generator

定义:

Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态,但是只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。

Generator 函数暂停恢复执行原理

要搞懂函数为何能暂停和恢复,那你首先要了解协程的概念。

一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。

协程是一种比线程更加轻量级的存在。普通线程是抢先式的,会争夺 cpu 资源,而协程是合作的,可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。它的运行流程大致如下:

  1. 协程A开始执行
  2. 协程A执行到某个阶段,进入暂停,执行权转移到协程B
  3. 协程B执行完成或暂停,将执行权交还A
  4. 协程A恢复执行

协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除 yield 命令,简直一模一样。

function* gen() {
  var result1 = yield "hello";
  var result2 = yield "world";
  return result1 + result2;
}
var g = gen();
g.next(1);
//{value : 'hello', done : false}
g.next(2);
//{value : 'world', done : false}
g.next();
//{value : 3, done: true}
g.next();
//{value : undefined, done: true}

闭包、垃圾回收和内存泄漏

定义:

闭包就是能够读取其他函数内部变量的函数。由于在 Javascript 语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成”定义在一个函数内部的函数“。

一、变量的作用域

要理解闭包,首先必须理解 Javascript 特殊的变量作用域。
变量的作用域无非就是两种:全局变量和局部变量。
Javascript 语言的特殊之处,就在于函数内部可以直接读取全局变量。
在函数外部自然无法读取函数内的局部变量,函数内部声明变量的时候,一定要使用 var 命令。如果不用的话,实际上声明了一个全局变量!

function f1() {
  var n = 999;
}
console.log(n); // error

function f2() {
  n = 999;
}
consoel.log(n); //999

二、如何从外部读取局部变量?

“链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

function f1() {
  var n = 2;
  function f2() {
    console.log(n);
  }
  return f2;
}

var result = f1();
console.log(result());

既然 f2 可以读取 f1 中的局部变量,那么只要把 f2 作为返回值,我们不就可以在 f1 外部读取它的内部变量了吗!

三、闭包的用途

  • 闭包可以用在许多地方。它的最大用处有两个
    • 一个是前面提到的可以读取函数内部的变量
    • 另一个就是让这些变量的值始终保持在内存中。
function f1() {
  var n = 999;

  nAdd = function () {
    n += 1;
  };

  function f2() {
    alert(n);
  }

  return f2;
}

var result = f1();

result(); // 999

nAdd();

result(); // 1000

在这段代码中,result 实际上就是闭包 f2 函数。它一共运行了两次,第一次的值是 999,第二次的值是 1000。这证明了,函数 f1 中的局部变量 n 一直保存在内存中,并没有在 f1 调用后被自动清除。
为什么会这样呢?原因就在于 f1 是 f2 的父函数,而 f2 被赋给了一个全局变量,这导致 f2 始终在内存中,而 f2 的存在依赖于 f1,因此 f1 也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
这段代码中另一个值得注意的地方,就是”nAdd=function(){n+=1}”这一行,首先在 nAdd 前面没有使用 var 关键字,因此 nAdd 是一个全局变量,而不是局部变量。其次,nAdd 的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以 nAdd 相当于是一个 setter,可以在函数外部对函数内部的局部变量进行操作。

四、使用闭包的注意点

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在 IE 中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

五、闭包常见的情况

var name = "The Window";

var object = {
  name: "My Object",

  getNameFunc: function () {
    return function () {
      return this.name;
    };
  },
};

alert(object.getNameFunc()()); //The Window
var name = "The Window";

var object = {
  name: "My Object",

  getNameFunc: function () {
    var that = this;
    return function () {
      return that.name;
    };
  },
};

alert(object.getNameFunc()()); //My object

闭包的高级写法

上面的写法其实是最原始的写法,而在实际应用中,会将闭包和匿名函数联系在一起使用。下面就是一个闭包常用的写法:

(function (document) {
  varviewport;
  varobj = {
    init: function (id) {
      viewport = document.querySelector("#" + id);
    },
    addChild: function (child) {
      viewport.appendChild(child);
    },
    removeChild: function (child) {
      viewport.removeChild(child);
    },
  };
  window.jView = obj;
})(document);

这个组件的作用是:初始化一个容器,然后可以给这个容器添加子容器,也可以移除一个容器。
功能很简单,但这里涉及到了另外一个概念:立即执行函数。 简单了解一下就行,需要重点理解的是这种写法是如何实现闭包功能的。
可以将上面的代码拆分成两部分:(function(){}) 和** ()** 。
第 1 个*() *是一个表达式,而这个表达式本身是一个匿名函数,所以在这个表达式后面加 *() *就表示执行这个匿名函数。
因此这段代码执行执行过程可以分解如下:

varf = function (document) {
  varviewport;
  varobj = {
    init: function (id) {
      viewport = document.querySelector("#" + id);
    },
    addChild: function (child) {
      viewport.appendChild(child);
    },
    removeChild: function (child) {
      viewport.removeChild(child);
    },
  };
  window.jView = obj;
};
f(document);

在这段代码中似乎看到了闭包的影子,但 f 中没有任何返回值,似乎不具备闭包的条件,注意这句代码:

window.jView = obj;

obj 是在函数 f 中定义的一个对象,这个对象中定义了一系列方法, 执行 window.jView = obj 就是在 window 全局对象定义了一个变量 jView,并将这个变量指向 obj 对象,即全局变量 jView 引用了 obj . 而 obj 对象中的函数又引用了函数 f 中的变量 viewport ,因此函数 f 中的 viewport 不会被 GC 回收,viewport 会一直保存到内存中,所以这种写法满足了闭包的条件。

总结

这是对闭包最简单的理解,当然闭包还有其更深层次的理解,这个就涉及的多了,你需要了解 JS 的执行环境(execution context)、活动对象(activation object)以及作用域(scope)和作用域链(scope chain)的运行机制。


冒泡事件和捕获事件

冒泡事件:(由内->外)

微软提出了名为事件冒泡(event bubbling)的事件流。事件会从最内层的元素开始发生,一直向上传播,直到 document 对象。
p -> div -> body -> html -> document

事件捕获(由外->内)

网景提出另一种事件流名为事件捕获(event capturing)。与事件冒泡相反,事件会从最外层开始发生,直到最具体的元素
事件冒泡和事件捕获过程图:
image.png

文章引用:

https://segmentfault.com/a/1190000015727237
https://juejin.im/post/6844903696111763470
https://juejin.im/post/6844903767226351623
https://www.jianshu.com/p/895422b87c7b
https://juejin.im/post/6844903960910757902


文章作者:   leader755
版权声明:   本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 leader755 !
评论
 上一篇
前端面试(3)vue 前端面试(3)vue
Vue: vue 数据双向绑定原理; vue computed 原理、watch 和 methods 的区别; vue 编译器结构图、生命周期、vue 组件通信; mvc 模式、mvp 模式、mvvm 模式; vue dom diff、vu
2020-08-20
下一篇 
Mac 解决GitHub速度慢问题 Mac 解决GitHub速度慢问题
解决方案是修改 hosts,按照以下三步来操作1.打开 hosts 文件:终端执行, MacOS 的登录用户不是 root,没有最高权限。修改一些系统保护的资源时,需要临时切换到 root 用户。root 具有最高权限,可以做任何事情。此时
2020-08-05
  目录