[TOC]

JS

typeof

typeof操作符返回一个字符串,表示未经计算的操作数的类型

类型 结果
Undefined "undefined"
Null "object" (见下文)
Boolean "boolean"
Number "number"
BigInt(ECMAScript 2020 新增) "bigint"
String "string"
Symbol (ECMAScript 2015 新增) "symbol"
宿主对象(由 JS 环境提供) 取决于具体实现
Function 对象 (按照 ECMA-262 规范实现 [[Call]]) "function"
其他任何对象 "object"

特别地:

typeof null; // "object"
typeof NaN; // "number"

NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN !== NaN 为 true

instanceof

instanceof可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型

console.log(2 instanceof Number);                    // false
console.log(true instanceof Boolean);                // false 
console.log('str' instanceof String);                // false 
 
console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);                   // true

instanceof只能正确判断引用数据类型,而不能判断基本数据类型。instanceof 运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性

构造器与new操作符

构造函数

构造函数在本质上是常规函数,不过有两个约定:

  • 命名以大写字母开头
  • 只能由new操作符执行
function User(name) {
  this.name = name;
  this.isAdmin = false;
}

let user = new User("Jack");

alert(user.name); // Jack
alert(user.isAdmin); // false

当一个函数被使用new操作符执行时,它按照以下步骤:

  • 创建一个新的空对象并分配给this
  • 函数执行时,通常会修改this,为其添加新的属性
  • 隐式return返回this的值

换句话说,new User(...) 做的就是类似的事情:

function User(name) {
  // this = {};(隐式创建)

  // 添加属性到 this
  this.name = name;
  this.isAdmin = false;

  // return this;(隐式返回)
}

所以 new User("Jack") 的结果是相同的对象:

let user = {
  name: "Jack",
  isAdmin: false
};

构造器的return

通常,构造器没有 return 语句。它们的任务是将所有必要的东西写入 this,并自动转换为结果。

但是,如果这有一个 return 语句,那么规则就简单了:

  • 如果 return 返回的是一个对象,则返回这个对象,而不是 this
  • 如果 return 返回的是一个原始类型,则忽略。

换句话说,带有对象的 return 返回该对象,在所有其他情况下返回 this

class语法

基础语法

class User {

  constructor(name) {
    this.name = name;
  }

  sayHi() {
    alert(this.name);
  }

}

// 用法:
let user = new User("John");
user.sayHi();

class的本质仍然是函数,class User{...}构造实际上做了如下的事

  • 创建一个名为User的函数,该函数成为类声明的结果。该函数的代码来自于 constructor 方法(如果我们不编写这种方法,那么它就被假定为空)
  • 存储类中的方法,例如 User.prototype 中的 sayHi

class

alert(User === User.prototype.constructor); // true
// 在原型中实际上有两个方法
alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi

class声明的函数与下面代码效果相同

// 1. 创建构造器函数
function User(name) {
  this.name = name;
}
// 函数的原型(prototype)默认具有 "constructor" 属性,
// 所以,我们不需要创建它

// 2. 将方法添加到原型
User.prototype.sayHi = function() {
  alert(this.name);
};

// 用法:
let user = new User("John");
user.sayHi();

原型与原型链

所有的实例对象都有一个__proto__属性,属性值是一个对象,指向对象的原型

所有函数都一个prototype属性,属性值为一个对象,指向函数的原型对象

所有实例对象的__proto__属性都指向其构造函数的prototype

原型图

重要公式:

Object.__proto__ === Function.prototype;
Function.prototype.__proto__ === Object.prototype;
Object.prototype.__proto__ === null;
function Proto() {
  function Basic() {
    this.type = "basic";
    this.desription = "基类";
  }
   // 修改 Basic 的原型
  Basic.prototype.getName = function () {
    return this.desription;
  };
  function Father() {
    this.type = "father";
    this.desription = "父类";
  }
  function Son() {
    this.type = "son";
    this.desription = "子类";
  }

  Father.prototype = new Basic();
  Son.prototype = new Father();

  let son = new Son();
   // instanceof 本质是从原型链上找
  console.log(`%c ${son instanceof Basic}`, "background: #222; color: #bada55");
  console.log("----son.constructor----", son.constructor);
  // 实例对象的原型,即Father
  console.info("----son.__proto__----", son.__proto__);
  // 方法的原型,即Father
  console.log("----Son.prototype----", Son.prototype);
  console.log(son.__proto__===Son.prototype) // true
  console.log("----son----", son);
  console.log("----son.getName()----", son.getName());
}
Proto()

原型与原型链

深拷贝和浅拷贝

浅拷贝:拷贝时只拷贝最外面一层数据的值,遇到深层次对象则拷贝其地址,改变源对象中的引用对象同时会改变目标对象的值,同样改变目标对象中的引用对象也会改变源对象的值

object.assigin() 为浅拷贝,只会把可枚举和自有(Object.hasOwnProperty())属性从一个或多个源对象拷贝至目标对象

Object.assign(target, ...sources)
function shallowCopy() {
  let obj1 = { a: 0, b: { c: 0 } };
  let obj2 = Object.assign({}, obj1);
  obj1.b.c = 2;
  // 浅拷贝:object.assigin()在拷贝时只拷贝其值,源对象为对象的引用时,拷贝其引用
  // 引用值发生改变目标对象值也会改变
  console.log(obj2); // { a: 0 , b: { c: 2}}
  obj2.b.c = 3;
  console.log(obj1); // { a: 0 , b: { c: 3}}

  let a = 2;
  let b = "id";
  let c = true;
  let copy = Object.assign({}, a, b, c); // 基本类型会被包装成对象
  console.log(copy); // { '0': 'i', '1': 'd' }
}

// shallowCopy();

实现浅拷贝

function shallowCopy(obj) {
  if (!obj || typeof obj !== 'object') return
  let newObj = obj instanceof Array ? [] : {}
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = obj[key]
    }
  }
  return newObj
}

深拷贝: 深拷贝会把对象里所有的数据重新复制到新的内存空间,是最彻底的拷贝

// 深拷贝
function deepCopy() {
  let obj1 = { a: 0, b: { c: 0 } };
  let obj2 = JSON.parse(JSON.stringify(obj1));
  obj1.a = 4;
  obj1.b.c = 4;
  console.log(JSON.stringify(obj2)); // { "a": 0, "b": { "c": 0}}
}

实现深拷贝

function deepCopy(obj) {
  if (!obj || typeof obj !== 'object') return
  let newObj = obj instanceof Array ? [] : {}
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key]
    }
  }
  return newobj
}

对象遍历

forin: 会遍历对象原型链上的属性 可以遍历数组但数组遍历建议用for of

hasOwnProperty: 判断对象自身是否包含某个属性,不包含原型链上的属性,有则返回true,无则返回false

Object.entries() 返回对象中可枚举属性的 键值对数组,与for in 不同的是不会遍历原型链上的属性

Object.keys() 对应的返回的是 对象的键的数组,但只会获取第一层的key

Object.values() 对应的返回的是 对象的值的数组,遇到引用对象则拷贝整个对象的值

// 对象遍历
function ObjectLoop() {
  let triangle = { a: 1, b: 2, c: 3 };
  let obj1 = { a: 1, b: { c: 2, d: { e: 3 } } };
  function ColoredTriangle() {
    this.color = "red";
  }
  ColoredTriangle.prototype = triangle;
  let obj = new ColoredTriangle();

  // for in 会遍历对象原型链上的属性 可以遍历数组但数组遍历建议用for of
  function forinLoop(obj) {
    console.log("------------forinLoop------------");
    for (var prop in obj) {
      console.log(`obj.${prop} = ${obj[prop]}`); // obj.color = red obj.a = 1 obj.b = 2 obj.c = 3
    }

    // hasOwnProperty 用法  对象自身是否包含某个属性,不包含原型链上的属性
    console.log("------------hasOwnProperty------------");
    for (var prop in obj) {
      if (obj.hasOwnProperty(prop)) {
        console.log(`obj.${prop} = ${obj[prop]}`); // obj.color = red
      }
    }
  }

  /* Object.entries() 返回对象中可枚举属性的 键值对数组,与for in 不同的是不会遍历原型链上的属性
   * Object.keys() 对应的返回的是 对象的键的数组 获取第一层的key
   * Object.values() 对应的返回的是 对象的值的数组
   */
  function entriesLoop(obj) {
    for (const [key, value] of Object.entries(obj)) {
      console.log("------------entriesLoop------------");
      console.log(`${key}: ${value}`);
    }
  }

  forinLoop(obj);
  entriesLoop(obj);

  console.log(Object.keys(obj1)); // [ 'a', 'b' ]
  console.log(Object.values(obj1)); // [ 1, { c: 2, d: { e: 3 } } ]
}

Object.defineProperty和Proxy

Object.defineProperty

MDN

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或修改对象的属性,并返回修改后的对象

Object.defineProperty(obj, prop, descriptor)

obj 要修改属性的对象

prop 要定义或修改的属性的名称

descripto 属性描述符

常见属性描述符: valueconfigurablegetsetenumerable

缺陷:

  • 无法监控新增的属性,由于该方法监听的是对象的属性,所以在原始对象上新增属性就无法劫持到

  • 对于数组的监听,在对数组进行sortshiftreverse等改变数组索引操作时会触发多次getset方法

  • 只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历

let arrShallow = [1, 2, 3, 4, 5, 6];
let arrDeep = [[1, 2, 3], 4, [5, 6]];

// Object.defineProperty  数组 劫持的是对象的属性,如果劫持的对象的属性的属性值为对象,则需深度遍历
function ArrayHijack() {
  function arrayProperty(arr) {
    for (let key in arr) {
      let value = arr[key];
      Object.defineProperty(arr, key, {
        get() {
          console.log(`get: arr[${key}]`);
          return value;
        },
        set(newValue) {
          console.log(`set: arr[${key}] to ${newValue}`);
          return (value = newValue);
        },
      });
    }
  }
  arrayProperty(arrShallow);

  arrShallow[0] = 999; // 打印:set: arr[0] to 999
  arrShallow[3]; // 打印:get: arr[3]
  arrShallow[8] = 8;
  // arr.shift(); // 会导致5次前移,所以产生5次get和5次set
  console.log(arrShallow[0]);

  arrayProperty(arrDeep);
  arrDeep[0][1] = 6;
  // 无法深度监听到数组的变化
  console.log("arrDeep[0]:" + arrDeep[0]); // get: arr[0] arrDeep[0]:1,6,3
}

// ArrayHijack();

// Object.defineProperty 监听的是对象的单个属性
function ObjectHijack() {
  let obj = {};

  Object.defineProperty(obj, "name", {
    get() {
      return value;
    },
    set(newValue) {
      return (value = newValue);
    },
  });

  console.log((obj.name = 6)); // 6
  console.log(obj.name); // 6
}

Proxy

MDN

Proxy() 用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)

const p = new Proxy(target, handler)

target: 要进行代理的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)

handler: 代理行为

常见handler:applygetsethas

handler.get(target,property,receiver)

target: 目标对象

property: 被获取的属性名

优势:proxy 是对整个对象进行劫持,并返回一个新对象

劣势:版本兼容性

// 浅劫持
function shallowProxy() {
  let obj = {
    count: 5,
    user: { name: "JavaScript", age: 22 },
  };

  let proxyObj = new Proxy(obj, {
    get(target, key) {
      console.log(`get:${key}`);
      return target[key];
    },
    set(target, key, value) {
      console.log(`set:${key} to ${value}`);
      return (target[key] = value);
    },
  });

  proxyObj.user.name = "Golang";
  /**浅劫持对象,如果读取到的属性为对象,需要深度劫持获取值
   * get: user
   * 相当于 proxyObj.user
   */
}

// 深度劫持
function deepProxyObject() {
  let obj = {
    count: 5,
    user: { name: "JavaScript", age: 22 },
  };
  function deepProxy(obj) {
    return new Proxy(obj, {
      get(target, key) {
        console.log(`get:${key}`);
        console.log(target[key]);
        if (typeof target[key] === "object" && target[key] !== null) {
          return deepProxy(target[key]); // 递归劫持
        }
        return target[key];
      },
      set(target, key, value) {
        console.log(target);
        console.log(`set:${key} to ${value}`);
        return (target[key] = value);
      },
    });
  }

  let proxyObj = deepProxy(obj);
  proxyObj.user.name = "Golang";

  proxyObj.fine = "i am fine";
}

call、apply、bind的区别

call()和apply()作用相同,传递参数不同

作用:

  • 改变this的指向
  • 实现继承
Function.prototype.apply(thisArg, argsArray) // 参数数组
Function.prototype.call(thisArg, arg1, arg2, ...) // 参数列表
Function.prototype.bind(thisArg, arg1, arg2, ...) // 参数列表

thisArg: 函数运行时的this值,可选

arg1: 传递的参数

function callApply() {
  function animal(name, type) {
    this.name = name;
    this.type = type;
  }

  function dog(name, type) {
    animal(name, type);
    this.year = 10;
  }

  console.log(new dog("哈士奇", "狗").name); // undefined

  function cat(name, type) {
    animal.call(this, name, type); // 继承
    console.log("---cat---", this.name, this.year);
  }

  function bird(name, type) {
    animal.apply(this, [name, type]);
    this.year = 20;
  }

  function display() {
    console.log(this.name, this.type);
  }

  console.log(new cat("加菲貓", "貓").name); // 加菲貓
  console.log(new bird("蜂鸟", "鸟").name); // 蜂鸟
  display.call({ name: "加菲貓", type: "貓" }); // 加菲貓 猫
}

MDN

bind() 创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用

  • bind方法能改变this的指向
  • call()和apply()是立即调用函数
  • bind()是创建一个新的函数

丢失this

let user = {
    firstName: "John",
    sayHi() {
      console.log(`Hello, ${this.firstName}!`);
    },
  };
user.sayHi(); // this 为user
setTimeout(user.sayHi, 200); // Hello, undefined! settimeout的this为window

这里丢失了user的上下文,实际上settimeout可以被重写为

let f = user.sayHi
setTimeout(f,200)
// 浏览器中的setTimeout方法,会为函数调用设定this为window

解决方案1:包装器

// 匿名函数
setTimeout(function() {
    user.sayHi(); // Hello, John!
},1000)
// 或者箭头函数
setTimeout(() => user.sayHi(), 1000); // Hello, John!

但是当后面的代码在settimeout调用方法之前,改变user的属性方法,就获取到的是更改后的值

setTimeout(() => user.sayHi(), 1000); // Hello, bob!
user.firstName = "bob"

解决方案2: bind

// this绑定为 user 也就是say 的this绑定为了 user,是对当前 user的绑定,后续代码对 user的更改不会影响 输出结果
let say = user.sayHi.bind(user);
say() // Hello, John! 
setTimeout(say,1000) // Hello, John!
user.firstName = "bob"
let username = {
  firstName: "bob",
};

let user = {
  firstName: "John",
  sayHi() {
    console.log(`Hello, ${this.firstName}!`);
  },
};

function sayHi(phrase) {
  console.log(`${phrase}, ${this.firstName}!`);
}

let say = sayHi.bind(username); // Hi, bob!
say("Hi");
sayHi.call(username, "call"); // 会立即执行sayHi函数
sayHi.apply(username, ["apply"]); // 会立即执行sayHi函数

this指向问题

this指向问题

this指向

  1. 严格模式与非严格模式

this在严格模式下与非严格模式下有细微的差别

// 非严格模式下
let a = 1
function f1(){
	console.log(this);
}
f1() // a
// 严格模式下
function f2(){
    "use strict";
	console.log(this);
}
f2() // undefined
  1. 普通函数下的this指向问题

this指向取决于最终函数的调用者,即取决于当前执行上下文

上下文:上下文类似于英语句子中的主语

const test = {
  prop: 42,
  func: function() {
    return this.prop;
  },
};

console.log(test.func()); // 42 // this为 test
const a = test.func(); 
console.log(a()); // 此时的this为 window 等同于 window.a()
  1. this用于闭包
var name = "pop"
function say() {
  var name = "bob"
  // 无法访问到外部函数的this变量
  return function () {
    console.log(this.name)
  };
}
let a = say()
a() // pop
  1. 箭头函数中的this指向问题

关于箭头函数

箭头函数中没有this绑定,必须通过查找作用域链来决定其值。 如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this,否则this的值则被设置为全局对象

var name = 'window'
var student = {
  name: '若川',
  doSth: function () {
    // var self = this;
    var arrowDoSth = () => {
      // console.log(self.name);
      console.log(this.name)
    }
    arrowDoSth()
  },
  arrowDoSth2: () => {
    console.log(this.name)
  },
}
student.doSth() // '若川'
student.arrowDoSth2() // 'window'

callapplybind无法直接改变箭头函数的this(它自身没有this),但可以通过改变箭头函数生成的普通函数的this,来改变this的指向

var name = 'window'
var student = {
  name: '若川',
  doSth: function () {
    console.log(this.name)
    return () => {
      console.log('arrow this', this.name)
    }
  },
}
let person = {
  name: 'person',
}

student.doSth().call(person) // 若川 arrow this 若川
let a = student.doSth.call(person) // person
a() // arrow this person
  1. 隐式函数绑定下丢失this上下文的问题
function foo() {
    console.log(this.a);
};
var obj = {
    a: 1,
    foo: foo
};
var bar = obj.foo; //传递了函数,隐式绑定丢失了

var a = 'hello';

bar(); // 'hello' 丢失了上下文

综合考察

var name = 'window'

var person1 = {
  name: 'person1',
  show1: function () {
    console.log(this.name)
  },
  show2: () => console.log(this.name),
  show3: function () {
    return function () {
      console.log(this.name)
    }
  },
  show4: function () {
    return () => console.log(this.name)
  }
}
var person2 = { name: 'person2' }

person1.show1()
person1.show1.call(person2)

person1.show2()
person1.show2.call(person2)

person1.show3()()
person1.show3().call(person2)
person1.show3.call(person2)()

person1.show4()()
person1.show4().call(person2)
person1.show4.call(person2)()

正确答案:

person1.show1() // person1
person1.show1.call(person2) // person2

person1.show2() // window
person1.show2.call(person2) // window

person1.show3()() // window
person1.show3().call(person2) // person2
person1.show3.call(person2)() // window

person1.show4()() // person1
person1.show4().call(person2) // person1
person1.show4.call(person2)() // person2

其中person1.show3()获取到的是一个函数,需要赋值给一个变量才会执行,加上()相当于立即执行函数,执行环境为window,所以打印window

相当于执行了以下操作

var fun = person1.show()
fun()

person1.show3().call(person2)person1.show3.call(person2)() 也好理解了。前者是通过person2调用了最终的打印方法。后者是先通过person2调用了person1的高阶函数,然后再在全局环境中执行了该打印方法

手写instanceof

instanceof

function _instanceof(left, right) {
  let _proto_ = left.__proto__;
  let prototype = right.prototype;
  while (true) {
    if (_proto_ === null) return false;
    if (_proto_ === prototype) return true;
    _proto_ = _proto_.__proto__;
  }
}

手写继承

Object.create() 方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)MDN

function inherit() {
  function Parent(name, age) {
    this.name = name;
    this.age = age;
    this.eat = function () {
      console.log(`${this.name} eat something`);
    };
  }
  Parent.prototype.say = function () {
    console.log(`${this.name} say my name is ${this.name} age ${this.age}`);
  };
  function Child(name, age, sex) {
    Parent.call(this, name, age);
    this.sex = sex;
  }
  // Child.prototype = new Parent(); // 与Object.create()效果相同
  Child.prototype = Object.create(Parent.prototype);
  // 更改需要利用constructor 指向 如果利用对象的形式修改了原型对象,需要利用constructor 指回原来的构造函数,即修复 constructor
  Child.prototype.constructor = Child;

  let stu = new Child("Bob", "male", 18);
  stu.say();
  stu.eat();
  console.log(stu.constructor.name); // Child
  // 如果没把Child.prototype.constructor重新指回Child,这里的值会是Parent
}

inherit();

手写call

let name = 'toy'
function person() {
  console.log(this.name)
  console.log(...arguments)
}

let human = { name: 'bob' }

Function.prototype.customCall = function (ctx) {
  // 判断 call 方法调用者是否为函数
  if (typeof this !== 'function') {
    console.error('type error')
  }
  // 这里的this指向为person,实际执行为person
  console.log(this)
  // 如果传入对象上下文不存在,则设置为 window
  ctx = ctx || window
  let args = [...arguments].slice(1)
  console.log(args)
  // 为 human 添加一个 person 方法
  ctx.fn = this
  // 执行该方法 并传入参数 返回运行结果
  let result = ctx.fn(...args)
  // 删除
  delete ctx.fn
  return result
}

person.customCall()
person.customCall(human, 'male', 18)

手写apply

Function.prototype.customApply = function (ctx) {
  // 判断 call 方法调用者是否为函数
  if (typeof this !== 'function') {
    console.error('type error')
  }
  // 这里的this指向为person,实际执行为person
  console.log(this)
  // 如果传入对象上下文不存在,则设置为 window
  ctx = ctx || window
  let args = arguments[1]
  console.log(args)
  // 为 human 添加一个 person 方法
  ctx.fn = this
  // 执行该方法 并传入参数 返回运行结果
  let result = null
  if (args) {
    result = ctx.fn(...args)
  } else {
    result = ctx.fn()
  }
  // 删除
  delete ctx.fn
  return result
}

person.customApply(human, ['male', 18])

手写bind

#

箭头函数

  • 没有this
  • 没有arguments
  • 不能使用new进行调用
// 箭头函数的this指向与常规变量的查找方式相同,即一层层往上找
let group = {
  title: "Our Group",
  students: ["John", "Pete", "Alice"],
  showList() {
    this.students.forEach(
      student => alert(this.title + ': ' + student)
    );
  }
};

group.showList();

ES6

块级作用域

解构赋值

异步

箭头函数

模块化

解构赋值

function destructArgs() {
  // 数组解构
  console.log("------数组解构------");
  [arr1, arr2, ...arrRest] = arrShallow;
  console.log(arr1, arr2, ...arrRest);
  [arr1, arr2] = [arr2, arr1];
  console.log("------数组交换------");
  console.log(arr1, arr2);
  // 对象解构
  console.log("------对象解构------");
  let options = {
    size: {
      width: 100,
      height: 200,
    },
    items: ["Cake", "Donut"],
    extra: true,
    name: {
      bob: true,
      pop: false,
    },
  };
  let {
    items: item,
    size: { width },
    ...restObject
  } = options;
  console.log(item, width, restObject);
}

rest参数

扩展运算符被用在函数形参上时,它还可以把一个分离的参数序列整合成一个数组。这一点经常用于获取函数的多余参数,或者像上面这样处理函数参数个数不确定的情况

function mutiple(...args) {
  console.log(args)
}
mutiple(1, 2, 3, 4) // [1, 2, 3, 4]

async、await和promise

宏任务:setTimeout、setInterval

微任务:Promise.then()

任务执行顺序:同步任务–> 微任务–> 宏任务

微任务会依次全部执行再执行下一个宏任务

async function sync() {
  let Promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("Promise1");
      resolve("resolve");
      console.log("object");
    }, 3000);
    console.log("111");
    resolve("Promise1End");
    reject("2");
    console.log("can?");
  })
    .then((res) => {
      console.log(res);
    })
    .catch((err) => {
      console.log(err);
    });
  // console.log(Primise1);
  console.log("同步1");
  console.log("同步2");
  let Promise2 = await new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("Promise2");
      resolve("end");
    }, 3000);
  });
  console.log(Promise2);
  console.log("同步3");
  // Promise.all([Promise1, Promise2]);
}

var、let和const

let和const的特点:

  • 不允许重复声明

  • 不存在变量提升

  • 暂时性死区

  • 块级作用域

var:

存在变量提升、可重复声明、无块级作用域

暂时性死区:

使用let、const声明的变量,必须先声明变量在使用

判断数组

Object.prototype.toString.call

MDN

toString()方法返回一个表示改对象的字符串

每个对象都有一个 toString() 方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 “[object type]”,其中 type 是对象的类型

console.log(Object.prototype.toString(Object)); // [object Object]
console.log(Object.prototype.toString(function () {})); // [object Object]
Object.prototype.toString.call('An') // "[object String]"
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call({name: 'An'}) // "[object Object]"

重写toString方法

function Dog(name, breed, sex, color) {
  this.name = name;
  this.breed = breed;
  this.sex = sex;
  this.color = color;
}

Dog.prototype.toString = function dogString() {
  return `Dog ${this.name} is a ${this.sex} ${this.color} ${this.breed}`;
};

let dog = new Dog("catty", "Lab", "male", "black");
console.log(dog.toString()); // Dog catty is a male black Lab

instanceof

instanceof 的内部机制是通过判断对象的原型链中是不是能找到类型的 prototype

使用 instanceof判断一个对象是否为数组,instanceof 会判断这个对象的原型链上是否会找到对应的 Array 的原型,找到返回 true,否则返回 false

instanceof 只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true

[]  instanceof Array; // true
[]  instanceof Object; // true

Array.isArray

MDN

Array.isArray([]) // true
Array.isArray({}) // false

EventLoop

JavaScript中,任务被分为两种,一种宏任务(MacroTask)也叫Task,一种叫微任务(MicroTask

宏任务

微任务、宏任务与Eventloop EventLoop

# 浏览器 Node
I/O
setTimeout
setInterval
setImmediate
requestAnimationFrame

微任务

# 浏览器 NOde
process.nextTick
MutationObserver
Promise.then catch finally

跨域

跨域的定义

当协议、域名、端口中任意一个不相同时,就会造成跨域。跨域是浏览器同源策略导致的。

同源:协议、域名、端口三者都相同

同源策略限制内容有:

  • Cookie、LocalStorage、IndexedDB 等存储性内容
  • DOM 节点
  • AJAX 请求发送后,结果被浏览器拦截了

但有三个标签运行跨域加载资源

  • <img src=XXX>
  • <link href=XXX>
  • <script src=XXX>

跨域解决方案

跨域的解决方案

  1. JSONP

原理:利用<script>标签没有跨域限制的漏洞,实现跨域

缺点:只支持get请求,不安全可能会遭受XSS攻击

实现:

function jsonp({ url, params, callback }) {
  return new Promise((resolve, reject) => {
    let script = document.createElement('script')
    window[callback] = function(data) {
      resolve(data)
      document.body.removeChild(script)
    }
    params = { ...params, callback } // wd=b&callback=show
    let arrs = []
    for (let key in params) {
      arrs.push(`${key}=${params[key]}`)
    }
    script.src = `${url}?${arrs.join('&')}`
    document.body.appendChild(script)
  })
}
jsonp({
  url: 'http://localhost:3000/say',
  params: { wd: 'Iloveyou' },
  callback: 'show'
}).then(data => {
  console.log(data)
})

上面这段代码相当于向http://localhost:3000/say?wd=Iloveyou&callback=show这个地址请求数据

  1. CORS

CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现

浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域

服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源

  1. postMessage

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的iframe消息传递
  • 上面三个场景的跨域数据传递

http://localhost:3000/a.html页面向http://localhost:4000/b.html发送信息

// a.html
  <iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe> //等它加载完触发一个事件
  //内嵌在http://localhost:3000/a.html
    <script>
      function load() {
        let frame = document.getElementById('frame')
        frame.contentWindow.postMessage('我爱你', 'http://localhost:4000') //发送数据
        window.onmessage = function(e) { //接受返回数据
          console.log(e.data) //我不爱你
        }
      }
    </script>
// b.html
  window.onmessage = function(e) {
    console.log(e.data) //我爱你
    e.source.postMessage('我不爱你', e.origin)
 }
  1. Nginx反向代理

实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。

使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。

实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录

  1. NodeJs中间件代理

实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。 代理服务器,需要做以下几个步骤:

  • 接受客户端请求
  • 将请求转发给服务器
  • 拿到服务器响应数据
  • 将响应转发给客户端

中间件代理

  1. websocket

Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了

本地文件socket.html向localhost:3000发生数据和接受数据

// socket.html
<script>
    let socket = new WebSocket('ws://localhost:3000');
    socket.onopen = function () {
      socket.send('我爱你');//向服务器发送数据
    }
    socket.onmessage = function (e) {
      console.log(e.data);//接收服务器返回的数据
    }
</script>
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');//记得安装ws
let wss = new WebSocket.Server({port:3000});
wss.on('connection',function(ws) {
  ws.on('message', function (data) {
    console.log(data);
    ws.send('我不爱你')
  });
})
  1. window.name + iframe
  2. location.hash + iframe
  3. document.domain + iframe

XLSX

安装

npm install xlsx

导入

import * as XLSX from 'xlsx'
import XLSX from 'xlsx'

新建工作簿

const wb = XLSX.utils.book_new();

数据格式转换

// json类型数据转换成sheet
let sheet1 = XLSX.utils.json_to_sheet(rows)
// json数据格式
const rows = [
    { '列1': 1, '列2': 2, '列3': 3 },
    { '列1': 4, '列2': 5, '列3': 6 }
]

// 二维数组
let sheet2 = XLSX.utils.aoa_to_sheet(ws_data)
// 数组格式
const ws_data = [
    ["姓名","性别","年龄"],
    ["蔡徐坤","男","60"],
    ["李明","男","30"]
]

添加工作表数据

// 参数:工作簿、sheet数据、工作表(sheet)名字
    XLSX.utils.book_append_sheet(wb,sheet1,['Sheet1'])

文件存储

XLSX.writeFile(wb, fileName);

性能优化

节流与防抖

防抖

事件频繁触发情况下,delay时间内被触发则会重新计时

// 防抖 设置方法执行间隔为 delay ms ,delay 时间内事件被触发则重新计时
function debunce(fn, delay) {
  delay = delay || 500;
  let timer = null; // 被闭包函数使用
  // 剩余参数数组
  return function () {
    let ctx = this;
    let args = [...arguments];
    clearTimeout(timer); // 如果 delay 秒内函数被执行则,会清除前面的settimeout
    timer = setTimeout(function () {
      console.log(args);
      fn.apply(ctx, args);
    }, delay);
  };
}

函数防抖就像回城,被打断就会重新计时

应用场景:

  • 按钮点击
  • 输入框

节流

规定事件在delay时间内只会执行一次

// 节流 设置方法执行间隔为 delay ms, delay 时间内时间被触发则只执行一次事件
function throttle(fn, delay = 500) {
  let timer = null;
  return function () {
    // 有 settimeout 则退出,不执行
    let ctx = this;
    let args = [...arguments];
    if (!timer) {
      timer = setTimeout(() => {
        console.log(args);
        fn.apply(ctx, args);
        timer = null;
      }, delay);
    }
  };
}

函数节流就像游戏中的攻击速度,规定一定时间内只能攻击多少次

应用场景:

  • DOM元素拖拽
  • 游戏中的刷新率
  • canvas画笔功能

加快首屏加载速度

Vue加快首屏加载速度

  1. 压缩
  • gzip压缩

  • 图片压缩

  1. 路由懒加载

  2. 优化分包策略

  3. CDN引入

  4. 优化分包

  5. SSR服务器渲染

  6. 增加宽带

回流与重绘

浏览器的渲染流程

回流与重绘

渲染引擎:主要有Gecko和Webkit,其中Firefox 使用的是 Gecko,而 Safari 和 Chrome 浏览器使用的都是 WebKit

渲染流程:

WebKit 渲染引擎的主流程

  1. 解析HTML Source,生成DOM树
  2. 解析CSS,生成CSSOM树
  3. 将DOM树和CSSOM树结合,去除不可见元素,生成渲染树(Render Tree)
  4. Layout(布局):根据生成的渲染树,进行布局(Layout),得到节点的几何信息(位置,大小)
  5. Painting(重绘):根据渲染树以及回流得到的几何信息,将 Render Tree 的每个像素渲染到屏幕上

回流(reflow)

渲染对象在创建完成并添加到渲染树时,是将DOM节点和它对应的样式结合起来,并不包含位置和大小信息。

我们还需要通过 Layout 布局阶段,来计算它们在设备视口(viewport)内的确切位置和大小,计算这些值的过程称为回流布局重排(reflow)

HTML 采用基于流的布局模型,从根渲染对象(即<html>)开始,递归遍历部分或所有的框架层次结构,为每一个需要计算的渲染对象计算几何信息,大多数情况下只要一次遍历就能计算出几何信息。但是也有例外,比如<table>的计算就需要不止一次的遍历

触发回流条件

DOM元素的大小位置发生变化的时候,会触发回流

改变这些属性会触发回流:

  • 盒模型相关的属性: widthheightmargindisplayborder
  • 定位属性及浮动相关的属性: top,position,float
  • 改变节点内部文字结构也会触发回流:text-align, overflow, font-size, line-height, vertival-align

以及进行以下流程或操作:

  • 页面一开始渲染的时候
  • 添加或删除可见的DOM元素,进行DOM操作等
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代
  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)
  • css伪类激活
  • 进行获取布局信息的操作,比如offsetWidthoffsetHeightclientWidthclientHeightwidthheightscrollTopscrollHeight,getComputedStyle, getBoundingClientRect

重绘(repaint)

绘制 paint:当各种盒子的位置、大小以及其他属性,例如颜色、字体大小等都确定下来后,浏览器便把这些元素都按照各自的特性绘制一遍,于是页面的内容出现了,这个过程也称之为 Repaint(重绘制)

说白了,页面要呈现的内容,统统画在屏幕上,这就叫 Repaint

触发绘制条件
  • DOM改动
  • CSS改动

其实,就是判断当视觉上是否发生变化(无论这个变化是通过DOM改动还是CSS改动)。只要页面显示的内容不一样了,肯定要 Repaint

回流一定会触发重绘,而重绘不一定会回流

渲染性能优化

注意:回流可以避免,但重绘无法避免,否则就成静态页面了

避免回流
  1. 减少对dom的操作
  2. 使元素脱离文档流
  3. 避免或减少访问某些属性
  4. css属性尽量使用简写
  5. transfrom代替lefttop,opacity代替visibility,使用tansfromopacity不会触发绘制
  6. 避免使用table布局
减少重绘
  • 如果需要创建多个DOM节点,可以使用DocumentFragment创建完,然后一次性地加入document。(加一个节点,就repaint一次,不太好)
  • 将元素的display设置为”none”,完成修改后再把display修改为原来的值
// 例1-使用 createDocumentFragment 方法创建虚拟的 dom 对象,将新 dom 需要修改的对象进行复制,然后对创建的 dom 进行相应的修改,最终在把 dom 与旧 dom 进行替换
  	// 这样的能将对 dom 的多次修改合并为一次,大大减少了回流和重绘的次数
    let box = document.querySelector('#box')
    let test = document.createDocumentFragment()
    for (let i = 0; i < 5; i++) {
        let li = document.createElement("li")
        li.appendChild(document.createTextNode(i))
        test.appendChild(li)
    }
    box.appendChild(test)
  
  
  	// 例2-把需要修改的 dom 隐藏,修改完成后再将 dom 重新显示
  	// 使用 display: none 后渲染树中将不再渲染当前 dom,所以多次操作也不会多次触发回流和重绘
  	let box = document.querySelector('#box')
    box.style.display = 'none';
  	for (let i = 0; i < 5; i++) {
        let li = document.createElement("li")
        li.appendChild(document.createTextNode(i))
        box.appendChild(li)
    }

浏览器原理

浏览器安全

XSS攻击

XSS

概念:Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全

XSS的本质是:恶意代码未经过滤,与网站正常的代码混在一起;浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行

XSS攻击方式:用户的输入内容,URL上的参数

XSS分类

XSS攻击可分为存储型反射型DOM型三种

存储型XSS

攻击步骤:

  1. 攻击者将恶意代码注入到目标网站的数据库中
  2. 用户打开目标网站时,网站服务器将恶意代码从数据库取出,拼接在HTML中返回给浏览器
  3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作

这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等

反射型XSS

攻击步骤:

  1. 攻击者构造出特殊的 URL,其中包含恶意代码。
  2. 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
  3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作

反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。

由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击

DOM型XSS

攻击步骤:

  1. 攻击者构造出特殊的 URL,其中包含恶意代码。
  2. 用户打开带有恶意代码的 URL。
  3. 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。
  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作

DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞

XSS攻击预防

XSS攻击有两大要素:

  1. 攻击者提交恶意代码
  2. 浏览器执行恶意代码

预防措施:

  1. 输入过滤

  2. 纯前端渲染

  3. 转义HTML

  4. 预防DOM型XSS攻击

    尽量避免使用.innerHTML.outerHTML,而应尽量使用.textContent.setAttribute

CSRF攻击

CSRF指的是跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过后台的用户验证,冒充用户向服务器执行一些操作

CSRF 攻击的本质是利用 cookie 会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充

浏览器缓存

概念

浏览器缓存

浏览器缓存:是指浏览器对用户请求过的静态资源(html、css、js),存储到电脑本地磁盘中,当浏览器再次访问时,就可以直接从本地加载了,不需要再去服务端请求

缓存的优点:

  • 减少了服务器的负担、提升网站性能
  • 加快客户端网页加载速度
  • 减少了冗余的数据传输,减少网费

缺点:

  • 容易导致客户端代码更新不及时

浏览器缓存分为协商缓存与强缓存

协商缓存与强缓存

强缓存

使用强缓存策略时,如果缓存资源有效,则直接使用缓存资源,不再向服务器发送请求

强缓存策略可以通过两种方式来设置,分别是响应头中的Expires属性和Cache-Control属性

Expires中的时间是一个绝对时间,它是服务器的时间,因此当客户端的时间和服务器的时间不一致,或者用户对客户端时间进行修改,这样可能会影响缓存命中的结果

Cache-Control的几个取值含义:

  • private: 仅浏览器可以缓存
  • public: 浏览器和代理服务器都可以缓存(对于private和public,前端可以认为一样,不用深究)
  • max-age=xxx 过期时间(重要)
  • no-cache 不进行强缓存(重要)
  • no-store 不强缓存,也不协商缓存,基本不用,缓存越多才越好呢
协商缓存

协商缓存条件:

  • Cache-Control的值为no-cache
  • 或者max-age过期了

使用协商缓存策略时,会先向服务器发送一个请求,如果资源没有发生修改,则返回一个 304 状态,让浏览器使用本地的缓存副本。如果资源发生了修改,则返回修改后的资源

协商缓存也可以通过两种方式来设置,分别是 http 头信息中的EtagLast-Modified属性

Etag是服务器根据当前请求的资源生成的一个唯一标识,这个唯一标识是一个字符串,只要资源有变化这个串就不同,跟最后修改时间没有关系

Etag的必要性:

使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,但是仍然有以下几个问题难以解决:

  • 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET
  • 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒)
  • 某些服务器不能精确的得到文件的最后修改时间

Etag的优先级比Last-Modified的优先级高

F5 会 跳过强缓存规则,直接走协商缓存;;;Ctrl+F5 ,跳过所有缓存规则,和第一次请求一样,重新获取资源

缓存判断流程

浏览器解析URL

用户输入URL,到浏览器呈现给用户页面,经历了哪些过程

  1. 用户输入url
  2. 对url地址进行DNs域名解析
  3. 进行TCP连接
  4. 进行HTTP报文的请求与响应
  5. 浏览器解析文档资源并渲染页面

Vue

生命周期

生命周期

生命周期.webp

vue2到vue3的变化

  • 组合式api
    • 响应式api:ref()reactive()
    • 生命周期钩子:onM0unted()onUnmounted
    • 解决了混入的缺陷
    • 减少了包的体积
  • 响应式原理 proxy
  • emit事件 需要先定义事件名
  • vue3可以有多个顶层标签
  • style里可以用(v-bind)关键字
  • :deep(.foo){}

和Mixin相比,mixins有三个主要短板

官方文档

  1. 不清楚的数据来源
  2. 命名空间冲突
  3. 隐式的跨mixin交流

MVVM、MVC、MVP的区别

MVVM

MVVMModelViewViewModel

  • MOdel代表数据模型,数据和业务逻辑都在Model层中定义
  • View代表UI视图,负责数据的展示
  • ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作

MVC

MVP

响应式原理

Vue2监听采用的是Object.defineProperty(),Vue3则是Proxy

采用数据劫持结合发布者订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的getter、setter,在数据变动时执行dep.notify()方法,发布消息给订阅者(Watcher),进行视图更新

Vue类

创建一个Vue类

class Vue{
    constructor(options){
       this.$el=options.el;
       this._data=options.data;
       this.$data=this._data;
       //对data进行响应式处理
       new Observe(this._data);
   }
}
//创建Vue对象
new Vue({
    el:'#app',
    data:{
      message:'hello world'
    }
})

Observe类

Observe进行数据监听

class Observe{
    constructor(data){
       //如果传入的数据是object
       if(typeof data=='object'){
           this.walk(data);
       }
    }
    //这个方法遍历对象中的属性,并依次对其进行响应式处理
    walk(obj){
        //获取所有属性
        const keys=Object.keys(obj);
        for (let i = 0; i < keys.length; i++) {
            //对所有属性进行监听(数据劫持)
            this.defineReactive(obj, keys[i])
        }
    }
    defineReactive(obj,key){
        if(typeof obj[key]=='object'){
            //如果属性是对象,那么那么递归调用walk方法
            this.walk(obj[key]);
        }
        const dep=new Dep();//Dep类用于收集依赖
        const val=obj[key];
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            //get代理将Dep.target即Watcher对象添加到依赖集合中
            get() {
              //这里在创建Watcher对象时会给Dep.target赋值
              if (Dep.target) {
                dep.addSubs(Dep.target);
              }
              return val;
            },
            set(newVal) {
                val=newVal;
                //依赖的变更响应
                dep.notify(newVal)
            } 
          })
    }
}

Dep类

Dep收集依赖,当Observer中的data触发getter时,Dep就会收集依赖的Watcher,当data变动时,就会通过DepWatcher发通知更新

class Dep{
   static target=null
   constructor(){
       this.subs=[];
   }
   addSubs(watcher){
       this.subs.push(watcher)
   }
   notify(newVal){
       for(let i=0;i<this.subs.length;i++){
           this.subs[i].update(newVal);
       }
   }
}

Watcher类

Watcher类用于观察数据的变更,它会调用data中对应属性的get方法触发Dep依赖收集,并在数据变更后执行相应视图更新

let uid=0
class Watcher{
    //vm即一个Vue对象,key要观察的属性,cb是观测到数据变化后需要做的操作,通常是指DOM变更
    constructor(vm,key,cb){
       this.vm=vm;
       this.uid=uid++;
       this.cb=cb;
       //调用get触发依赖收集之前,把自身赋值给Dep.taget静态变量
       Dep.target=this;
       //触发对象上代理的get方法,执行get添加依赖
       this.value=vm.$data[key];
       //用完即清空
       Dep.target=null;
    }
    //在调用set触发Dep的notify时要执行的update函数,用于响应数据变化执行run函数即dom变更
    update(newValue){
        //值发生变化才变更
        if(this.value!==newValue){
            this.value=newValue;
            this.run();
        }
    }
    //执行DOM更新等操作
    run(){
        this.cb(this.value);
    }
}

通过以上代码即可实现一个简易版的Vue

let data={
    message:'hello',
    num:0
}
let app=new Vue({
    data:data
});
//模拟数据监听
new Watcher(app,'message',function(value){
    //模拟dom变更
    console.log('message 引起的dom变更--->',value);
})
new Watcher(app,'num',function(value){
    //模拟dom变更
    console.log('num 引起的dom变更--->',value);
})
data.message='world';
data.num=100;

示例

class Watcher {
  constructor(vm, exp, cb) {
    this.vm = vm;
    this.exp = exp;
    this.cb = cb;

    // 在创建Watcher实例时,将Watcher实例本身添加到Dep中
    Dep.target = this;

    // 通过getter方法获取组件中用到的数据属性的值
    this.value = this.get();

    // 清空Dep.target,以便在下一个Watcher实例创建时能够正确的添加到Dep中
    Dep.target = null;
  }

  // 通过getter方法获取组件中用到的数据属性的值
  get() {
    const value = this.vm[this.exp];
    return value;
  }

  // 当数据属性发生变化时,触发回调函数
  update() {
    const oldValue = this.value;
    const newValue = this.vm[this.exp];

    if (oldValue !== newValue) {
      this.value = newValue;
      this.cb.call(this.vm, newValue, oldValue);
    }
  }
}

// Dep类用于维护数据属性和Watcher实例之间的依赖关系
class Dep {
  constructor() {
    this.subs = [];
  }

  // 将当前的Watcher实例添加到Dep中
  addSub(sub) {
    this.subs.push(sub);
  }

  // 触发所有Watcher实例的回调函数
  notify() {
    this.subs.forEach(sub => {
      sub.update();
    });
  }
}

// 在getter方法中,将Watcher实例添加到对应的Dep实例中
function defineReactive(obj, key, val) {
  const dep = new Dep();

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      if (Dep.target) {
        dep.addSub(Dep.target);
      }
      return val;
    },
    set(newValue) {
      if (val !== newValue) {
        val = newValue;
        dep.notify();
      }
    }
  });
}

// 示例数据
const data = {
  message: 'Hello, world!'
};

// 将数据对象中的每个属性都转换为getter/setter
Object.keys(data).forEach(key => {
  defineReactive(data, key, data[key]);
});

// 创建Watcher实例,用于监听message属性的变化
const watcher = new Watcher(data, 'message', (newValue, oldValue) => {
  console.log(`message属性发生变化:${oldValue} => ${newValue}`);
});

// 修改message属性的值
data.message = 'Hello, Vue!';

总结

其实在 Vue 中初始化渲染时,视图上绑定的数据就会实例化一个 Watcher,依赖收集就是是通过属性的 getter 函数完成的,文章一开始讲到的 ObserverWatcherDep 都与依赖收集相关。其中 ObserverDep是一对一的关系, DepWatcher 是多对多的关系,Dep 则是 ObserverWatcher 之间的纽带。依赖收集完成后,当属性变化会执行被 Observer 对象的 dep.notify() 方法,这个方法会遍历订阅者(Watcher)列表向其发送消息, Watcher 会执行 run 方法去更新视图

reactive

虚拟DOM

虚拟DOM是对真实DOM的一种抽象,本身就是一个js对象

优点:将真实节点抽象成VNode,有效减少直接操作dom次数,提高程序性能