js 总结

83 阅读24分钟

发布订阅模式

基于一个事件中心,要接收通知的对象通过定义事件订阅,然后保存到事件中心,发布通知的对象触发后,通过事件中心执行里面的订阅事件

const Events = {
  listenList: {},
  age: 16,
  listen: (key, fn) => {
    if (!Events.listenList[key]) {
      Events.listenList[key] = [];
    }
    Events.listenList[key].push(fn);
  },

  emit: (key, ...value) => {
    const listenList = Events.listenList[key];
    if (listenList) {
      listenList.forEach((item) => item(...value));
    }
  },

  remove: (key, fn) => {
    if (!Events.listenList[key]) return false;
    if (!fn) {
      delete Events.listenList[key];
    } else {
      const currentList = Events.listenList[key];
      currentList.forEach((fn, i) => {
        if (fn === fn) {
          currentList.splice(i, 1);
        }
      });
    }
  },
};

Events.listen("houser", (args) => {
  console.log("args houser", args, "---->housrer");
});

Events.listen("car", (args) => {
  console.log("args car", args, "---->car");
});

Events.emit("houser", 2500000);
Events.emit("car", 2500000);

发布订阅模式,一般是先订阅然后发布,但是,发布者发布事件的时候,不关心有没有订阅,如果没有订阅也能发布,订阅者不管心有没有事件发布,只要自己的事件发生,他就执行。所以,发布订阅模式中,发布者和订阅者互不关心对方,耦合度低,订阅者也可以自定义事件处理程序。

缺点: 当事件越来越多时候,命名不规范可能会导致不好维护。

观察者模式

观察者模式分为观察者和被观察者

被观察者相当于发布者, 当被观察者的状态变化的时候,会主动去通知所有的观察者, 将参数传给他们,然后执行让观察者执行他们自己的方法。

观察者和被观察者耦合在一起,必须要被观察者中注册了观察者后,才能和该观察者进行参数传递,并且还是当被观察者状态变化后,主动通知

// 观察者模式 内部基于发布订阅

class Subject {
  // 被观察者

  constructor() {
    this.arr = [];
  }

  attach(fn) {
    // 被观察者要接收观察者
    this.arr.push(fn);
  }

  setState(args) {
    this.arr.forEach((item) => item.update(args));
  }
}

class Observer {
  // 观察者
  update(args) {
    console.log("被观察者说 " + args, "--->args");
  }
}

let observer1 = new Observer("第一个观察者");
let observer2 = new Observer("第二个观察者");

let subject = new Subject();
// 搜集观察者
subject.attach(observer1);
subject.attach(observer2);

subject.setState("啦啦啦");

函数柯里化

// 柯里化函数其实相当于一个高阶函数
// 用高级函数提前把其中一个变量写好。调用的时候只需要填写一个变量就好

const add = (x, y, z, m) => {
  return x + y + z + m;
};

// console.log(add(1, 2, 3, 4));

const curry = (x) => {
  return (y) => {
    return (z) => {
      return (m) => {
        return x + y + z + m;
      };
    };
  };
};

// console.log(curry(1)(2)(3)(4));

// 要判断当前传入函数的参数个数 (args.length)
// 是否大于等于原函数所需参数个数 (fn.length) ,
// 如果是,则执行当前函数;如果是小于,则返回一个函数。

const curryFun = (fn) => {
  const curry = (...args) => {
    if (args.length >= fn.length) {
      return fn(...args);
    } else {
      return (...a) => curry(...args, ...a);
    }
  };

  return curry;
};

const curry1 = curryFun(add);

console.log(curry1(1, 2, 3)(4));
console.log(curry1(1)(2)(3)(4));
console.log(curry1(1)(2)(3, 4));

高阶函数中 包含柯里化函数,柯里化函数就是一个高阶函数

柯里化的含义就是让一个函数变的更具体一些, 原则上返回的函数只能接受一个参数,多个参数也暂且任务是柯里化sum(1)(2)

偏函数: 返回一个函数,函数的参数不止一个 sum(1,2)(3,4)(5)

反柯里化: 让函数的作用范围变大

高阶函数可以赞存变量(内部有闭包)

function isType(type) {
	return funcion(value) {
    return Object.prototype.toString.call(value) === `[object ${type}]`
  }
}

let isString = isType('String');
isString('Number')


通用的柯里化函数

就是根据调用的时候传递的参数,和函数的参数做判断,如果传递的参数和定义的参数一致,就让函数执行。

function sum(a,b,c,d,e) {
return a + b + c + d + e;
}

const curring = (fn) => {
	let args = []; // 存储用户调用的参数
	const inner = (args = [], ...arr) => {
	  arr.push(...arr)
	  // 如果传递的参数个数大于等于定义的参数,就让原函数执行,如果不是就返回一个函数
	  return args.length >= fn.length ? fn(...args) : (...o) => inner(args, ...o)
	 //   return args.length >= fn.length ? fn(...args) : inner;
	}
	return inner;

}

let newSum = curring(sum)
let fn1 = newSum(1,2)
const curry = fn => {
	const inner = (args = []) => {
		return fn.length <= args.length ? fn(...args): (...a) => inner([...args, ...a])
	}
	return inner()
}

原型原型链

javaScript 中遵循了几种规则

  • 所有的数据都是对象
  • 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它
  • 对象会记住他的原型
  • 如果对象无法响应某个请求,会把这个请求委托给他自己的原型

所有引用类型都有__proto__属性,是一个对象,里面有一个constructor指向他的构造函数

只有函数有prototype属性,他也是一个对象,里面有一个constructor指向他自己

所有引用类型的__proto__属性都指向他的构造函数prototype

Prototype 原型

是函数的一个属性,值是一个对象

const fn = Function Fn() {};
fn.prototype // fn.prototype 是一个对象  {constructor: f}

每一个函数,都会有一个默认的prototype的属性,是一个对象(箭头函数除外)

proto

是Object的一个属性 也是一个对象

每一个对象,都有一个——proto——的属性,是一个对象

let obj = {age: 3}
obj.__proto__  //  {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}

对象的——proto——的值是该对象的构造函数的prototype

obj.__proto__ === Object.prototype // true
function Test() {}
let test = new Test();
test.__proto__ === Test.prototype;
// Test.prototype 是一个对象,所以他也有__proto__
Test.prototype.__proto__ === Object.prototype  // true
Object.prototype.__proto__ // null 原型链顶层是null

特殊情况

typeof Object // "function"
typeof Function // "function"


const fn = new Function()
fn.__proto__  // ƒ () { [native code] }
Function.prototype  // ƒ () { [native code] }

这里fn 怎么也会有proto的属性呢,这里就要说下new 操作符了

在new的时候,new操作符做了下面这几件事情

  • 首先创建了一个对象
  • 然后将对象的proto 属性设置为了 构造函数的prototype
  • 然后将this绑定在创建的对象上
  • 若函数没有返回这个对象则返回this

这个时候,我们看到,fn.proto 属性其实等于 Function.prototype

fn.__proto__ === Function.prototype // true
let Test = new Function()
Function._proto_  // ƒ () { [native code] }
Function.prototype //  ƒ () { [native code] }
Function._proto_ === Function.prototype // true

Object._proto_ // ƒ () { [native code] }
Object._proto_ === Function.__proto__
Object._proto_ === Function.prototype

原型链

自己本身没有的属性,会顺着自己的--proto-- 属性一直往上找

如果给Test.prototype 中加一个属性。Test.prototype.a = 1; 则。test.__proto__就会多一个a =1 的属性

function Test() {}
let test = new Test();
test.constructor === Test  // true
test.a = 1;
Test.prototype.b = 2;
test.b // 2
test.hasOwnProperty('a') // true
test.hasOwnProperty('b') // false
'b' in test // true;
'a' in test // true

如果我们想要只判断自己本身有没有属性。则需要用hasOwnProperty 来判断。

如果判断 属性在不在自身或者自己的原型上,则可以用 in

constructor属性

function Test() {}
let test = new Test();
test.__proto__.constructor === Test // 对象的__proto__里的constructor属性指向他的构造函数

总结

每一个构造函数都有一个prototype的属性,每一个对象都有一个——proto__属性,构造函数实例化出来的对象的--proto-- 指向 构造函数的prototype,实例化出来的--proto--对象中的constructor 属性是他的构造函数,寻找对象的属性的时候会按照--proto--依次向上寻找,构成的链就是原型链。原型链的终点是null

迭代器iterator

当我们遍历数组的时候,用for 循环的时候,如果多个循环嵌套,则需要有多个循环变量,每次需要追踪多个变量,所以,利用迭代器可以消除这种问题。本质上其实迭代器也是一个对象,而且Es6也提出了for...of 循环,来配合遍历迭代器.与for循环相比,for...of不需要追踪值在集合中的位置,也不需要控制循环结束的时机。

迭代器

迭代器是一种对象,是一种专有接口,所有的迭代器对象都有一个next() 方法,调用next() 方法后会返回一个结果对象,这个对象包含两个值,一个是value,值是要返回的当前值,一个是done,done 是一个布尔值,如果还有可返回的value,则done返回false,如果没有,则done返回false, 当done 返回true的时候,value 是函数调用过程中最后一次给调用者传递的参数,如果没有,则返回undefined.

我们来自己创建一个迭代器,其实十分简单,返回一个迭代器对象。

const arr = [1, 2, 3];

const myIterator = (item) => {
  let i = 0;
  return {
    next: () => {
      let done = i >= item.length;
      let value = done ? undefined : item[i++];
      return {
        value,
        done,
      };
    },
  };
};

let iter = myIterator(arr);
console.log(iter.next(), "--->1"); // { value: 1, done: false } --->1
console.log(iter.next(), "---->2"); // { value: 2, done: false } ---->2
console.log(iter.next(), "---->3"); // { value: 3, done: false } ---->3
console.log(iter.next(), "---->3"); // { value: undefined, done: true } ---->3

迭代器对象除了具有next() 方法外,还具有return() 方法和throw()方法,不过return()方法和throw()方法不是必须的

return() 方法

如果for...of 循环出错或者有break语句导致提前退出,就会调用return()方法。return()方法必须返回一个对象。但是return() 的返回值是不会生效的。

const myIterator = (item) => {
  let i = 0;
  return {
    next: () => {
      let done = i >= item.length;
      let value = done ? undefined : item[i++];
      return {
        value,
        done,
      };
    },
    return: () => {
    	console.log('提前退出')
    	return {done: true}
    }
  };
};

let obj = {age: '1', name: 'ss'};
obj[Symbol.iterator] = () => { 
    return myIterator([1,2,3,4])
}

// 返回1, 调用return 方法,打印提前退出
for (let item of obj) {
	console.log(item) // 1
	break;
}

throw() 方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法。

Symbol.iterator

Iterator 为所有的数据结构提供了一种统一的访问机制,我们可以利用 for...of 来进行循环遍历。

当我们用for...of 遍历对象的时候,就会提示报错,但是遍历数组或者map, set, 字符串等就可以,是因为数组,map, set,字符串部署了Iterator 接口,简单来讲,就是他们都有一个内置的Symbol.iterator属性

如果对Symbol属性还不了解的,可以点击这里学习~

let arr = {age: '1', name: 'ss'}
for (let item of arr) {
  console.log(i) // TypeError: arr is not iterable
}

let array = [1,2,3];
// 和arr对象不同的是,我们可以在array.__proto__下看到,有一个Symbol(Symbol.iterator): values() 属性

原生默认具备Symbol.iterator属性的数据结构有

  • Array
  • Map
  • Set
  • String
  • 函数的arguments对象
  • NodeList对象

一个数据结构只要有Symbol.iterator属性,就可以认为是可遍历的,就可以使用for...of 来进行遍历。Symbol.iterator 的值就是一个返回迭代器对象的函数(也可以用生成器函数)

// 我们把上面刚刚写的myIterator函数赋值给obj的Symbol.iterator
let obj = {age: '1', name: 'ss'};
obj[Symbol.iterator] = () => { 
    return myIterator([1,2,3,4])
}

// 然后用for...of 遍历 没有报错
for (let item of obj) {
	console.log(item) // 1,2,3,4
}

我们也可以改变他的Symbol.iterator属性。

let str = '你捉不到的this';
str[Symbol.iterator] = () => {
	return myIterator([1,2,3,4])
}
for (let item of str) {
	console.log(item) // 1,2,3,4
}

模拟for of

实现for...of 也很简单,获取到Symbol.iterator属性的迭代器对象然后遍历

const myForOf = (item, callback) => {
  if (typeof callback !== "function") {
    return new TypeError("callback is not function");
  }

  if (typeof item[Symbol.iterator] !== "function") {
    return new TypeError(`${item} is not iterator`);
  }

  let getIteratorObj;
  let result;
  getIteratorObj = item[Symbol.iterator]();

  result = getIteratorObj.next();
  while (!result.done) {
    callback(result.value);
    result = getIteratorObj.next();
  }
};

myForOf([1, 2, 3, 4], (i) => {
  console.log(i, "--->myForOF");
});

生成器

上面我们自己手动编写了一个简单的迭代器对象,但是实际上的迭代器编写规则是十分复杂的,所以,ES6 还提供了一个生成器函数,用来创建返回迭代器对象。

生成器默认会给Symbol.Iterator属性赋值,因此所有通过生成器创建的迭代器都是可迭代对象

生成器是一个返回迭代器的函数

他有几个特征。一个是 function关键字后与函数名之间有一个* 号,调用方式和普通函数方式调用相同,函数体内部使用yield 表达式,来定义不同的内部状态,相当于是一个状态机

function * createInterator() {
  console.log(1);
  yield 1;
  console.log(2);
  yield 2;
  console.log(3);
  yield 3;
  // return 4  则next() 返回{value: 4, done: true}
}

let r = createInterator();

r.next() // 1, {value: 1, done: false}
r.next() // 2, {value: 2, done: false}
r.next() // 3, {value: 3, done: false}
r.next() // {value: undefined, done: true}

// 注意: 每次调用next()。都会执行到yield 语句,然后不在执行,直到调用下一个next 方法,并且返回一个对象,value值为yield的返回值。如果函数最后return 的话,调用next 返回的value是函数最后return 的值

// 当执行next 的时候,会执行到遇到yield表达式的那行,后面的代码暂停,然后yield 后面表达式的值作为返回对象的value, 下一次调用next,继续往下执行,知道遇到下一个yield。 如果没有新的yield,直到遇到return,将return 值当成value值,并且done 为true, 如果继续next(),则返回{value: undefined, done: true}
let obj = {
	create: function *(){
		console.log('--->生成器')
	}
}
let obj1 = {
	*create(){
		console.log('--->生成器')
	}
}

不可以使用箭头函数来创建生成器函数

yield

Yield 只能在生成器内部使用,在其他地方使用会抛出错误,即使在生成器函数的内部函数中使用也不可以

next()

next()方法可以传入一个参数,参数表示的是上一个yield表达式的值(也就相当于上一个yield表达式返回的值)。如果给第一次调用next()方法传入参数,是无效的,会被忽略。因为第一次调用next() 方法前不会执行任务yield语句

function * create(x){
    yield x + 1;
    yield x + 2;
    let z = yield 3;
    yield z + 5;
}

let c = create(1);
c.next() // {value: 2, done: false}
c.next() // {value: 3, done: false}
c.next() // {value: 3, done: false}
c.next() // 没有传入参数,相当于undefined,所以上一次yield的返回值z = undefined, undefined + 5 = NAN, 所以返回 {value: NaN, done: false}

throw()

生成器函数返回的迭代器对象含有一个throw方法,可以将它抛出的错误传递给生成器函数内部,在内部进行捕获。

调用throw()方法后,会抛出一个错误,后面的代码是否执行,取决于生成器函数内部的代码,如果将错误捕获,就会接着执行,不捕获就不执行

function * create(){
    yield 1;
    yield 2;
    try {
      yield 3;
    } catch(err) {
      yield 4;
    }
    yield 5;
}
let r = create()
r.next() // {value: 1, done: false}
r.throw(new Error('xxx')) // Error
r.next() // {value: undefined, done: true}  如果内部没有捕获错误,下次nextdone直接会变为 true

return

如果生成器函数中return, 则return后面的语句不再执行,如果return了值,则值是value的值

function * create() {
	yield 1;
	yield 2;
  console.log('2')
	return 3;
}

let r = create();
r.next() // {value: 1, done: false}
r.next() // {value: 2, done: false}
r.next() //  2 {value: 3, done: true}

Set

提供的一种新的数据结构集合

Set 本身是一个构造函数,用来生成Set 数据结构,类似于数组,但是里面的值是唯一的,没有重复的值(内部用到了Object.is 方法来进行判断, 除了对 +0 和 -0 的判断和Object.is 不一致)

创建Set

const set = new Set();

Set构造函数的参数也可以接受一个数组或者具有 iterable 接口的可迭代对象(包括其他集合)

const set = new Set([1,2,3,4,5]) // Set {1,2,3,4,5}
const set1 = new Set(set) // Set {1,2,3,4,5}

向set 中添加成员,且通过Object.is比较,不会添加重复的值,并且添加后不会修改

// 始终返回调用他的集合
set.add()
set.add(0).add('1').add('2') // 可以连续调用
// 可以添加任意值, undefined,null, {}, [], Symbol, NaN, string, number, boolean,function

Object.is(+0,-0) // false
let obj = {}
set.add(obj)
obj.name = 'lalala'
set // 成员不会随着obj属性修改而修改

获取Set实例的成员数

set.size  // 5

删除Set实例中的某个成员

// 删除成功返回true, 删除失败返回false
set.delete(value)

判断Set实例中是否含有某个成员

// 存在返回true, 失败返回false
set.has(0)

清空所有的成员

// 没有返回值
set.clear()

遍历Set实例的方法

Set类是可迭代的,所以可以用for of 来进行枚举集合的所有元素,也可以通过扩展运算符或者Array.from来对set 进行转换为数组。

虽然集合不能向数组一样取得集合中的第几个元素,但是Set 会记住元素插入的顺序,而且始终按这个顺序迭代集合

回调函数方式

forEach()

// 利用回调函数的方式遍历每一个成员
set.forEach((item, index, arr) =>{
	console.log(item, index, arr)
})

// 因为set集合没有索引,所以item 和index 都是set 成员的值

迭代器方式

利用迭代器的方式迭代,返回一个迭代器对象,返回的迭代器对象就可以用for of 来遍历

  • Keys()
// 返回一个迭代器对象
set.keys() // 返回所有的键名,因set没有键名,只有键值,所以返回值

  • Values() // 是默认迭代器 所以可以直接使用...操作或者直接用for..of 遍历

    用[Symbol.iterator]用来获取默认迭代器

    set[Symbol.iterator] === set.values
    set.values() // 键名和键值一样
    
  • Entries()

    set.entries()  // 键名和键值一样
    

WeakSet

如果在set中保存了一个对象成员,但是在有的时候,并不希望一直保存这个对象的引用,这时候,WeakSet就派上了用场,他保持了其弱引用,比如,获取了一个dom的节点并且保存在set中,如果元素在dom树中被删除,但是这个保存的节点对象引用却会仍然存在set中,浪费了内存,无法得到回收,而weakSet却可以在其他地方都没有引用到这个对象的时候,立即释放其内存

WeakSet()构造函数与Set()构造函数类似,也是不重复的值的集合,他实现了一组对象,并且不会妨碍这些对象被垃圾回收程序收集,他只有add(), has(), delete()方法,并且没有size 属性,并且不可迭代

创建WeakSet

let ws = new WeakSet()

初始化WeakSet

构造函数可以接收一个可以迭代的对象(具有Iterable接口的对象)对象中的每个值都会按照迭代顺序插入到WeakSet中去 (注意: 每个值只能是非null的对象类型,否则报错)

const v1 = {name: 'v1'}
const v2 = {name: 'v2'}
const v3 = {name: 'v3'}
const vs1 = new WeakSet([v1,v2,v3]) // 成功
const vs2 = new WeakSet([v1,4]) // TypeError
// 如果初始化的时候只要有一个不是Object类型,则会抛出错误,导致初始化失败

WeakSet 的成员只能是对象或者是继承自Object的类型 (为了保证通过值对象引用才可以取到值。如果允许原始值,则就会没办法区分初始化时使用的字符串字面量和初始化之后使用的是一个相等的字符串了)

添加某个成员

let ws = new WeakSet()
ws.add('x') // 添加一个非对象值会直接抛出TypeError
let x = new String('xxx')
ws.add(x) // WeakSet { String } // 添加成功
ws.add({}) // WeakSet { String, {}} // 添加成功

// add 方法返回集合实例,所以可以操作连起
const v1 = {name: 'v1'}
const v2 = {name: 'v2'}
const v3 = {name: 'v3'}
ws.add(v1).add(v2).add(v3)

删除某个成员

// 删除成功返回true, 删除失败返回false
set.delete(v1)

判断是否存在某个成员

// 存在返回true, 失败返回false
const obj = {name: 'lalala'}
ws.add(obj)
ws.has(obj) // true
ws.has('xxx') // false

当成员中的对象没有指向这个对象的其他引用的时候,这个对象就被当作垃圾回收,这个值就会在集合中消失

不可迭代

因WeakSet中的任意值任何时候都可能被销毁,所以没有迭代的方法,不能遍历,并且也没有clear方法

Map

对象只能用字符串当作键, 所以用数字1和字符串“1”当作属性名,都会被强制转为字符串类型

除此之外,还有一些细微的区别

  1. map的插入速度一般来说比object快
  2. 大量操作下,object的查找速度比map快
  3. map的删除操作比插入和查找更快,比对象删除好用
  4. 同内存下。map的存储键值对比object多存储50%

Map类型是一种存储着键值对的有序列表,是一种新的集合类型。其中的键名和键值可以是任意的数据类型。

创建map

const m = new Map()

初始化的时候可以向map构造函数传入一个数组, 或者传入一个具有Iterator接口,且成员是一个具有双元素的数组

const m = new Map([]) // Map {}
const m = new Map([['name', 'lalala'],['age', 26]]) // Map {name => lalala, age => 26}

// 实际上是把 let arr = [['name', 'lalala'],['age', 26]]
arr.forEach(([key, value]) => m.set(key, value))

添加新的成员

// set 方法,接收两个参数,键名和键值
m.set('name', 'lalala') 
// set 返回实例,可以把多个操作连缀起来

// 如果只传入一个参数,则该参数作为键名,键值为undefined
m.set('age') // Map {age => undefined}

m.set(null, null) // Map {null => null}
m.set(undefined, undefined) // Map {undefined => undefined}

// 如果键名已经存在,则set的时候会把原来的键值覆盖
m.set('a', 1)   // Map {'a' => 1}
m.set('a', 222) // Map {'a' => 222}

// 对象也可以当作键名
let obj = {age: 18}
m.set(obj, obj) // Map {Object => Object}
obj.age = 26
m // Map {{age: 26} => {age: 26}}
// 如果key或者value 是对象,对象里面的属性修改,map里面的值也会修改

获取map中的成员

m.get(key) // 根据key值获取value

// 如果key值不存在,返回undefined 
m.get('xxx') // undefined
m.set(1,22)
m.get(1) // 22

获取map 的成员总数

m.size  // 0

判断指定的键名在Map集合中是否存在

// 存在返回true, 不存在返回false
m.set('a',1) 
m.has('a') // true
m.has('xxxx') // false

删除集合中指定键名的成员

// 删除成功返回true, 删除失败返回false
m.delete('a') // true
m.delete({}) // false

移除所有的成员

m.clear() // 无返回值

遍历Map实例的方法

Map的遍历顺序就是插入顺序,即迭代的第一个键值对是最早添加到映射中,最后一个键值对是最晚添加的。与set 的方法相似。也有迭代器方式和回调函数

回调函数forEach方式

m.forEach((value, key, map) => {
	console.log(value, key, map) // 第一位是value值,第二个是key值,第三个是māp
})
// 传入的回调接受可选的第二个参数,用来指定内部this的指向

迭代器方式

  • keys()

    // 返回所有键名的迭代去对象
    for (let key of m.keys()) {
    	console.log(key)
    }
    
  • values()

// 返回键值的迭代器对象
for (let values of m.values()) {
	console.log(values)
}
  • entries()

    entries()是map默认的迭代方式

m[Symbol.iterator] === m.entries

所以可以用for..of 或者扩展运算符转为数组

m.set('a', 11)
// 返回成员的迭代器对象
for (let a of m.entries()) {
	console.log(a) // ['a', 11]
}
for (let [key, values] of m.entries()) {
	console.log(key, values) // 'a'  11
}

WeakMap

新增的一种新的集合类型,弱映射,和weakSet相似。属于Map的一个变体,他不会阻止键值被当作垃圾回收,当键值的引用都不存在的时候,就会被释放内存,如果只用对象作为键名,那么weakMap是最好的选择

主要是实现值与对象的关联不导致内存泄漏

键必须为非null类型的对象或者数组, 不能把原始值作为键,键名对应的值可以是任意类型

初始化方法

cont wm = new WeakMap()
// 默认传入一个数组,里面的元素都是包含两个元素的数组,且第一个元素必须是对象,不然就会抛出TypeError
const vm1 = new WeakMap([[{}, 'a'],[{}, 'b']])

添加成员方法set

// 返回成员实例。可以连续set
vm.set({}, 'aa')

get

// get 获取到为value. 如果获取不到返回undefined
let obj = {};
vm.set(obj, 'aa')
vm.get(obj) // 'aa'

has

// 判断是否有这个值 有返回true, 没有返回false
vm.has(obj) // true

delete

// 删除成功返回true, 删除失败返回false
vm.delete(obj)  // true

没有size属性,因为其大小可能随着对象被当作垃圾收集而随时改变

不可迭代

因WeakMap中的任意值任何时候都可能被销毁,所以没有迭代的方法,不能遍历,也没有clear 方法

如有错误,还请指出,共同进步~

看到这里啦,点个赞吧~

common.js

使用require()函数导入其他模块,通过设置Export对象的属性或者完全替换module.exports对象来导出公共的API

导出模块

Node定义了一个全局的exports对象{},这个对象始终会有定义,如果一个模块要导出多个值,可以直接把这些值设置为exports对象的属性

// a.js

console.log("这是a.js文件");

const sum = (a, b) => {
  return a + b;
};
const name = "你捉不到的this";


// 利用exports对象导出
exports.sumFun = sum; // 导出sum函数
exports.userName = name; // 导出name
exports.toFixedNumber = (number) => {
  return number.toFixed(2);
};

module.exports 的默认值和exports引用的是同一个对象。即 module.exports == exports

所以,在导出的时候,可以直接导出一个对象,而不是用exports属性的方式一个个导出

const sum = (a, b) => {
  return a + b;
};
const name = "你捉不到的this";

module.exports = {
	sum,    // 导出sum 函数
	userName: name  // 导出name, 并将其重命名为userName
}

导入模块

利用require导入导出的文件模块, 参数是其他要导入模块的名称,返回值是该导入模块的导出值

const FileA = require('./a');  // 可以将整个文件导入
// FileA 是一个对象,里面包含了导出的属性
FileA.sum // function
FileA.userName // '你捉不到的this'

// 也可以通过解构赋值的方式,只导入打算使用的特定属性
const { userName } = require('./a');
const { sum: sumFun } = require('./a'); // 也可在重命名

导出的是一个对象值的拷贝

一旦输出一个值,模块内部的变化就影响不到这个值了,因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成

// a.js
let name = '你捉不到的this';
let changeName = () => {name = 'lalala'}

module.exports = {
 name,
 changeName,
}

// index.js
const fileA = require("./a");
console.log(fileA.name); // '你捉不到的this'

fileA.changeNum();

// name 并没有变成lalala
console.log(fileA.name) // '你捉不到的this'

注意: 这里是一个值的浅拷贝。如果导出的是一个对象。还是会修改的

// a.js
let name = {age: '你捉不到的this'};
let changeName = () => {name.age = 'lalala'}

module.exports = {
 name,
 changeName,
}

// index.js
const fileA = require("./a");
console.log(fileA.name); // {age: '你捉不到的this'}

fileA.changeNum();

// name.age 变成lalala
console.log(fileA.name) // {age: 'lalala'}


导入的时候是同步加载

(require的内容会阻塞后续代码的执行,只有加载完成才会继续执行)

从导入的文件依次同步执行代码, 比如依次导入a.js, b.js

// a.js
console.log('a.ja')

// b.js
console.log('a.ja')


// index.js
const fileA = require("./a"); // 这里fileA是{}
console.log('1---->')
const fileB = require("./b"); // {} // 动态加载
console.log('index.js')

// 最终会先输出 a.js, 然后输出'1--->',然后输出b.js, 最后才输出index.js

运行时候加载(动态导入)

只有等代码运行的时候才能确定导入。不能进行tree shaking,所以可以在运行的代码中require

先整体加载a.js文件,加载里面的所有方法,生成一个对象_a,然后从这个对象上面去读取两个方法,只有在运行时候才能得到这个对象,所以没办法在编译的时候做静态优化

ES6模块

ES6 为JavaScript添加了import 和export 关键字

在浏览器中支持度不同,所以,es6语法会被babel 转换为common,js语法,但是因为为浏览器环境中并没有 module、 exports、 require 等环境变量。所以common.js语法在浏览器中不被允许,还需要we b pa c k进行打包,webpack 通过模拟这些变量进行打包执行

ES6导出

只需要在需要导出的声明前面加上export关键字就可以

export const name = '九思'
export const changeName = () => {}

或者只用一个export 语句申明真正要导出的值

const name = '九思'
const changeName = () => {}
const age = '18'
export {
  name,
  changeName,
	age as userAge, // 导出标识符重命名
}

注意这里的花括号实际上不会定义对象字面量,这个语法只是要求在一对花括号中给出一个逗号分隔的标识符列表

如果要导出一个值,可以使用默认导出,使用默认导出可以导出任意表达式,包括匿名函数和匿名类表达式,或者导出对象字面量

每一个模块都只能设置一个默认的导出值

const changeName = () => {}
export default changeName;

es6模块输出的是值的引用

ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。原始值变了,import 加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块


export let name = '你捉不到的this';
export let changeName = () => {name = 'lalala'}


// index.js
const {name, changeName } = require("./a");
console.log(name); // '你捉不到的this'

changeNum();

// name 变成lalala
console.log(name) //  'lalala'

ES6导入

使用import 关键字可以将导出的模块在另一个模块中访问,import的语句有两个部分,一个是要导入的标识符和标识符应当从哪个模块导入,import命令具有提升效果,会提升到整个模块的头部,首先执行。

import { name, changeName } from './a.js';

// 也可以在导入的时候重新命名
import { name as userName } from './a.js';

导入后无法在此模块定义另一个重名变量,并且也无法在import语句前使用标识符,也不能给导入的绑定重新赋值

import { name } from './a.js';
name = 'lalala'  // 抛出错误
// 如果name 是一个对象。该变去name的属性是可以的

也可以将整个模块作为一个单一的对象导入,然后模块里面的导出都可以按照对象的属性访问

import * as modelTest from './a.js';

// modelTest.name

不管import 语句中把一个模块写多少次,该模块只会执行一次,导入模块的代码执行后,实例化的模块被保存在内存中,不管引用多少次都是使用相同的模块实例

import { name } from './a.js';
import { changeName } from './a.js';

模块中即导出了默认值,也导出了其他的时候,当引用的时候,默认值必须排在非默认值前面

// a.js
export const name = 'lalala';
export default function(a, b) {
	return a + b
}

// index.js
import sum, { name } from './a.js'

如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。

export { sum, name as userName } from './a.js';

不可动态导入导出

它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。让他确定哪些可以导出导入

import 函数

因import和export 语句是在编译时候,所以提出了import()函数,来支持动态加载模块,返回一个promise 对象,它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块

console.log('la la la')

if (name === 'lalala') {
  import('./a.js').then((res) => {
   console.log(res)
  }).catch((err) => {
    console.log(err)
  })
}

比如react 中用到的利用react.lazy 配合suspense, 或者路由懒加载

const LazyComponent = React.lazy(() => import('./LazyComponent'));

function demo () {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <LazyComponent />
      </Suspense>
    </div>
  )
}

  1. common.js导出的是一个对象的浅拷贝,es6 导出的是对象的引用地址

  2. common.js是同步加载,同步导入的,es6 是在import之前,先对代码进行静态分析,遇到模块加载命令import,就会生成一个只读的引用,等到脚步真正执行的时候,根据这个只读的引用去被加载的那个模块里面取值,他属于动态引用,并不会缓存值, 所以可以进行threeshaking

  3. Common.js可以进行动态导入,es6不能动态导入,可以利用import()函数实现动态加载模块

Symbol

前言

Symbol 是ES6 中新增的一个新的原始类型,用来创建必须通过 Symbol才能引用的属性。在学习的过程中对Symbol这块的知识做了一些总结,如果你对JavaScript中这个新的类型还不太熟悉,就来和我一起学习一下吧~~

背景

我们先开看下,为什么会新增这样一个类型,之前的javascript中只有 string, number, boolean, null 和undefined 这五种原始类型,当我们用对象进行属性命名的时候,其key值都是字符串,这样的话就容易造成命名冲突,为了实现私有属性,保证每个属性的名字都不想等,所以新标准中新增了symbol

创建Symbol

Symbol 通过全局的Symbol函数创建。但是Symbol属于原始值,所以,当我们调用 new Symbol()的时候会导致程序抛出错误,也不能给他添加属性

const s = Symbol();
let obj = {};
obj[s] = '私有属性值';
obj // { Symbol(): '私有属性值' }
obj[s] // '私有属性值'
typeof s  // "symbol"

这里我们创建了一个名称为s的symbol,将其作为一个属性给赋值到obj上面,当我们要访问的时候,直接使用最初创建的symbol 来进行访问

当我们创建的时候,还可以给symbol中添加一些描述,用来记录。Symbol的描述被存储在内部的[[ Description]]属性里面,只有当调用他的toString()

方法的时候才可以读取到,在执行console打印的时候,其实是隐式的调用了他的toString方法。当然也可以用实例属性description,来直接返回描述

let s = Symbol('九思')
console.log(s) // Symbol(九思)
s.toString() // Symbol(九思)
s.description // '九思'

如果描述是一个对象的话,则在创建的时候,Symbol 会先调用对象的toString()方法,然后再插入描述

let obj = {age: 18}
obj.toString() // "[object Object]" 
let s = Symbol(obj)
s // Symbol([object Object])

判断Symbol 类型,es6扩展了其对应的typeof 操作符,可以直接判断 typeof Symbol() === "symbol"

由于以 Symbol 值作为键名,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法,当Symbol 作为属性名的时候,当遍历对象时候,for...in, for...of,Object.keys() 并不会返回Symbol属性名,只能用Object.getOwnPropertySymbols(obj)方法来获取所有的Symbol属性名,该方法返回一个包含所有Symbol自有属性的数组。

Symbol共享

有时候,我们希望在不同的代码中共享同一个Sym bol值,ES6为我们提供了一个随时可以访问的全局Symbol注册表,利用Symbol.for()方法创建。接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值,如果有,就返回这个Symbol值,否则就新建一个将其注册到全局。后续如果传入同样的键调用Symbol.for(),会返回相同的Symbol

let s = Symbol.for('id');
let obj = {
 [s]: '你捉不到的this'
}
s // "Symbol('id')"
let s2 = Symbol.for("id");
s === s2 // true
s2 // "Symbol('id')"

可以使用Symbol.keyFor()方法在Symbol 全局注册表中检索与Symbol 有关的键,如果存在返回ke y,如果不存在返回undefined

let s = Symbol.for('id')
Symbol.keyFor(s) // id
let s = Symbol('id1')
Symbol.keyFor(ss) // undefined

排序算法

排序算法

将无序状态的一列数据转化为有序状态

按复杂度分类

o(n^2)

  1. 插入排序
  2. 比较排序
  3. 冒泡排序

o(nlgn)

  1. 合并排序
  2. 快速排序
  3. 分块排序

o(n) o(nk)

  1. 桶排序
  2. 基数排序

原地

  1. 插入排序
  2. 比较排序
  3. 冒泡排序
  4. 快速排序

非原地

  1. 合并排序
  2. 桶排序
  3. 分块排序
  4. 基数排序

数据结构的存储方式

数据结构存储底层实现只有两种

  1. 数组 (顺序存储)
  2. 链表 (链式存储)

其他的数据结构图,树,散列表等,其实上都是利用这两种结构进行特殊操作。

数组: 存储是连续的,可以随机访问,也可以通过索引来快速找到其对应的元素,但是因为连续存储,所以他的内存空间必须一次性分配够,不然后面数组扩容,只能分配一块更大的空间,然后把数据全部复制过去,时间复杂度为o(n),并且数组中插入和删除的操作,会导致需要移动操作元素的后面所有的元素用来保证他的连续性,时间复杂度也是o(n)

链表: 元素不连续,靠指针指向下一个元素位置,所以不需要扩容,当操作插入或者删除的时候,只需要知道元素的前驱和后驱就可以了,时间复杂度是o(1), 但是没办法通过索引找到元素,不能随机访问,而且存储的时候需要存储指向前后元素的指针,空间会消耗更多