JS总结

本文仅供自己复习使用

关于解释型语言和编译型语言
编译型语言:只编译一次,后续只需要执行第一次编译的 exe 文件即可,如 C,说得直白一点大致就是 点菜,菜上齐了再吃
解释型语言:运行时才开始翻译,每次都要重新翻译,而且是逐句逐句的翻译的,如 JavaScript,说得直白一点就是 烫火锅

一、变量

1、变量类型

总共8大变量类型,七个基本,一个引用
基本类型(存在栈中):NullUndefinedNumberStringBooleanSymbol(es6确定)、BigInt(es10提出,es11确定)
引用类型(存在堆中):Object

关于 undefined 与 Undefined
Undefined 是类型,而平常我们赋值时使用的 var a = undefined 中的undefined 是变量,而非关键字。对于 ES5 之前的标准及老版浏览器中 undefined 可以被赋值。而真正值为 undefined 的可以通过 void 表达式,为了保险我们通常用void 0void(0) 来表示真正的undefined

关于 null 与 Null
Null 是类型,同时 null 也是关键字,所以直接用 null 不会被修改

关于 Undefined 和 Null
Undefined 表示声明了,未被定义,一般情况下很少有把变量赋值为 undefined 的,但是对于箭头函数如果只有一句话,且无返回值,可以使用var a = () => void aMethod();这样就可以不用写大括号了
Null 表示定义了,但值为空
null == undefinednull !== undefined
0 == '', 0 !== ''

关于 String
其实严格意义上来讲 String 不能被称为字符串类型,而应该被称为字符串中的 UTF16 编码类型
所以使用 charAt、length、charCodeAt 等方法时数出来的个数不对
String 能接受的最大长度是 2^53 - 1,但实际上能识别的却是 0 - 65535
这是因为 JS 引擎执行过程有三个阶段,分别为词法语法分析阶段 -> 预编译阶段 -> 执行阶段,在预编译阶段的长度首先就会被限制,毕竟内存没那么大,在执行期间也会被限制,所以最后长度就不可能有 2^53 - 1 那么多

关于 Number
如果想判断是 +0 还是 -0,可以使用 1 / x,如果得到的是 -Infinity 就说明是 -0,如果是 Infinity 就是 +0
浮点数不能通过 == 或 === 来比较,因为精度问题,但是相比较是否相等可以使用Math.abs(0.1 + 0.2 -0.3) <= Number.EPSILON
为什么不能直接比较 0.1 + 0.2 === 0.3 呢
Number类型通过IEE754的64位来表示数字
在这里插入图片描述
第0位:符号位,0表示正数,1表示负数
第1-11位:表示指数
第12-63位:储存小数部分即有效数字

如图展示在Number.MAX_VALUE、Number.MAX_SAFE_INTEGER等的取值问题
在这里插入图片描述
另外整数最大安全数是 Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1,而不是 Math.pow(2, 52) - 1,虽然尾数是52位
这是因为尾数部分默认为1,可省略不写,所以最大数就是(52位尾数+1位为1的被省略的数)
所以在 JS 中, Math.pow(2, 53) === Math.pow(2, 53) + 1
计算时发生了什么?
计算机无法对十进制进行计算,所以要根据 IEE754 转为二进制
精度损失可能出现在进制转化和对阶运算(指将两个进行运算的浮点数的阶码对齐)过程中

关于 Symbol
一切非字符串的对象的 key 的集合,一般通过 Symbol 函数创建,如 var a = Symbol('hello'),另外每一个 Symbol 类型的值都是独一无二的,Symbol(1) != Symbol(1)
for of 也跟 Symbol.iterator 有关,因为某对象的 Symbol.iterator 方法指向该对象的遍历器方法,而只要有遍历器方法就能是用 for of

	var obj = {}
	obj[Symbol.iterator] = function() {
		var v = 0;
		return {next:v++,done:v>10}
	}

补充遍历器(Iterator)相关:
Iterator 其实是为各种数据结构提供统一的访问机制,ES6提出的 for … of 就需要实现 Iterator 接口,如果想对一个数据结构使用 for of 或迭代器,可以使用var a = Iterator(b)
数组,字符串,arguments,set,map 都有 Iterator,都能使用 for of
使用三点运算符和解构赋值也是默认去调用 iterator 接口
普通的对象不是可迭代对象,不可以用 for of,但是可以用 for in 取出对象的键
工作原理:①创建一个指针对象(遍历器对象),指向数据结构的起始位置
②第一次调用 next 方法,指针自动指向数据结构的第一个成员
③接下来不断调用 next 方法,指针会一直往后移动
④每次调用 next 方法返回一个包含 value 和 done 的对象,遍历结束后返回 {value: undefined, done: true}

关于for…in
for…in会把原型上的也遍历出来

关于装箱
装箱后的数据和原始数据是不同的,如 3 和 Number 就不一样
我们有时候会见到以下的代码

	var a = '555'
	console.log(a.toString())

总所周知基本类型是没有属性和方法的,毕竟他们已经是值了,那么这里是如何调用的toString()方法呢,其实这里js引擎做了三步:
新建一个装箱的实例
执行该方法
删掉该实例
这个步骤有点像call、bind、apply的实现

关于 valueOf 和 toString
如果是 String 则先使用 toString,若无法得到原始类型,再使用 valueOf,若 valueOf 也得不到,则报 TypeError
如果是其他如 Function、Boolea、Number 等类型,则先使用 valueOf,在使用 toString,都得不到则报错
另外,关于 3.toString() 会报错是因为 JS 引擎理解为 3.(数字字面量) 和 toString(),所以如果想真正实现就要用 3..toString() 或者使用 3 .toString(3和点中间有个空格,空格代表空白符号,使3变成一个单独的 token,.toString() 也变成单独的token)
数字字面量:11.1.0

为何引入 BigInt
因为最大的安全数字为 Number.MAX_SAFE_INTEGER,超过这个数字的计算就会出现计算出错,为了解决这个问题就引入了 BigInt

关于类型转换
NaN == falseNaN == true结果都是false
但是Boolean(NaN) == false
({}) == true({}) == false结果都是false
而且{} == true或者{} == false会报错,因为{}会被认为是作用域
[] == true
[]{} 转为 boolean (用Boolean函数)是 true
在这里插入图片描述
在这里插入图片描述
但如果是放在if括号里面会把最后结果通过Boolean()转换类型

{} + [] === 0 这个其实就相当于 [].toNumber
({}) + ([]) === '[object Object]' 这个相当于({}).toString() + [].toString()
{} + ([]) === 0 跟第一个一样,都是 [].toNumber
({}) + [] === '[object Object]' 跟第二个一样
({}) + 1 === '[object Object]1' 相当于 ({}).toString() + 1..toString()
{} + 1 === 1 就是 1.toPrimitive
[] + 2 === '2' 就是 [].toString() + (2).toString()
其中将()里放{}让内核认为这是一个对象,如果直接用{}就会被认为这是代码块
+操作只有让两个值为 Number 或 String 才行
①首先让两个值去 ToPrimitive 也就是说如果是原始值就直接返回,如果不是就通过 valueOf 转换成原始类型,如果是引用类型就用 toString,之后把另外一个也 toString
②如果第一步没有用到 toString,只要有任何一个值为 String 类型,两个就都要转为 String
③如果没有,那就使用 toNumber

关于类型判断
typeof a:无法区分 null数组对象Date类型RegExp类型 ,都会被认为是 'object’
a instanceof Number:只能检查是否为该值隐式原型(__proto__)链上的一个,拿不到具体的
a.__proto__.constructor:除了nullundefined都可以判断出来,但是有时候重写prototype,会遗失constructor,就默认指向Object了
Object.prototype.toString.call(a):会返回[Object xxx],这个xxx就是具体类型


2、变量、函数、类声明

var:会声明提前,可重复声明,可修改,无块级作用域,只有函数作用域和全局作用域

let:不会声明提前,不可重复声明,可修改,有块级作用域

const:不会声明提前,不可重复声明,值不可修改(若为引用类型,则是地址不可修改,但内部指向的属性可变),有块级作用域
在这里插入图片描述

class:不会声明提前,不可重复声明,块级作用域,不可以在声明之前调用这个值,哪怕这个在父级作用域上也不行,会在预编译的阶段就报错,如下

	var c = 1;
	function foo(){
	    console.log(c);   //报错:VM2300:3 Uncaught ReferenceError: Cannot access 'c' before initialization
		//但是把 class c 删掉就行
	    class c {}
	}
	foo();

另外:在 class 中直接声明函数,不能加 function,应该直接用 a(){} 这种简便写法,否则报错,class 中找不到 function 标识符

function:普通函数声明,可重复声明,会生成一个执行上下文,每次执行都会有一个新的 AO ,只要没有闭包,其 AO 就会销毁

闭包满足:①函数嵌套,②内部函数引用了外部函数的数据
闭包不会再执行后马上销毁,而是放在内存中,也就是说下次使用时不需要再次创建

运行一个函数之前,会做以下操作 函数形参赋值 -> 函数声明 -> 变量声明,这些操作将创建一个函数的 AO(即活动对象)
其中,变量声明的时候,如果有同名的函数被声明了就不要覆盖声明

关于变量提升,本文最开始有说明 JS 是解释型语言,但 JS 依旧有变量提升,且值被默认设为 undefined,流程如下:
(1)词法和语法分析阶段
①将代码分词(也称为词法分析),简单来讲就是调用 next() 方法,把每个字都拿到,然后和 JS 的关键字和保留字等比较。在这一步中会自动过滤掉空格和注释,最后拿到一个 token 数组(或者列表)
语法分析,如果语法有错,则报错,否则生成一个 AST 树
③形成词法作用域(只跟定义时所处的位置有关)
(2)预编译阶段
①创建执行上下文,JS 会以栈的形式对执行上下文处理,称为函数调用栈(也称为执行上下文栈),执行上下文中有三个东西:作用域链、变量对象、this
②给执行上下文创建变量对象(AO)函数形参赋值(arguments + 形参) -> 函数声明 -> 变量声明(值为 undefined)
③给执行上下文创建作用域链,即当前函数的变量对象(AO) + 之前函数调用栈中的变量对象
④通过调用位置和调用方法来创建this
(3)执行阶段
①通过当前执行上下文作用域链对变量赋值或取值
②根据执行逻辑生成 CPU 能理解的机器码
③执行即可


二、原型

A 的 prototype 是 A_prototype
a(A 的实例)的 __proto__ 是 A_protoype
因为 A_protoype 其实也是通过 new Object 得到的对象,所以 A_protoype.__proto__ 是 Object_protoype
同时 Object 的 prototype 也是 Object_prototype
Object_prototype.__proto__ 是 null,相当于 Object_prototype 没有原型(万恶之源)
Function 的 prototype 是 Function 的 __proto__
Function_prototype 的 __proto__ 是 Object_prototype
在这里插入图片描述
补充:箭头函数没有原型(prototype),也不能用 new 去构建对象,箭头函数内部没有 arguments 对象


三、回调地狱

异步函数多层直接嵌套,不利于阅读,如图
在这里插入图片描述
解决方法:①通过 Promise 的链式调用法则来解决

关于 Promise

	var a = new Promise(function(resolve, reject){
		resolve();
		reject();
	})

①在 Promise 的参数如果没有碰到 return 就要一直执行下去,但是第一次如果碰到 resolve() 就把状态从 pending -> fulfilled,如果碰到 reject() 就把状态从 pending -> rejected,如果碰到 return 就马上返回,若 return 前有状态,则执行 thencatch ,若 then 中有某一行有异常,会在这一行终止,然后进入 catch(但如果 catch 写在 then 之前就不会,因为 thencatch 也各自返回一个 promise 对象)。若在 promise 参数里没有遇见 resolverejected 该状态则还是 pending

promise.then(fn1, fn2) 可以接受两个函数,第一个函数代表 fulfilled,第二个为 rejected,返回的 promise 对象是什么状态就是什么状态,
1)如果返回一个值,那么就是 fulfilled 状态
2)如果没有返回值,则自动返回一个 fulfiledpromise对象,不过最好还是自己返回一个自定义的 promise 对象
3)如果抛出一个错,就是 rejected 状态
4)如果返回 Promise.resolve(),那么就是 fulfilled 状态
5)如果返回 Promise.reject(),就是 rejected 状态

promise.catch(fn) 捕获 rejected 状态,其实这个内部调用的是 promise.then(undefined, fn)

Promise.all([promise1, promise2]) 返回一个 promise,如果有一个 rejectedpending,这个返回的 promise 就是 rejectedpending,如果成功,则返回结果数组(该数组与最开始定义时的顺序相同),若失败则返回第一个失败的值

Promise.race([promise1, promise2]) 哪儿个先完成就返回哪个的状态及其值,管他是成功或是失败


②通过 async 和 await 来解决

定义的 async 函数在同步没执行完前就是一个 pending 状态的 promise,同步队列都执行完了,才执行 async 函数
async 函数返回一个值,则会使用 Promise.resolve() 包装一下,await 只能写在 async 函数中
await 里面的 promise 如果有某一个状态为 pending 则直接跳出且返回的 promisepending 状态
如果不显式返回一个值,或显示返回一个 promise 最后 async 函数得到的值里就是空的


③通过 generator 函数

调用 generator 函数获得一个 Iterator 对象,通过 next 获取每个 yield 的值

	function* a{
		yield 2;
		let x = yield 'aaa';
		yield x;
	}
	console.log(a);     // Generator{}
	console.log(a.next());    // 2
	console.log(a.next());   // 'aaa'   此时 x = 'aaa'
	console.log(a.next(99));  // 99
	console.log(a.next());  // undefined

四、关于异步

异步分为宏观和微观。我们通常把我们自身所发起的任务称为宏观任务,把由JS 引擎发起的任务称为微观任务。
先微观后宏观
微观任务:promise、process.nextTick (Node 的)、async
宏观任务:setTimeout、setInterval、I/O、UI渲染、Script 标签里的


五、脚本与模板

脚本就是直接用 <script> 标签里的代码,脚本中不能写 importexport
模板就是用 importexport<scirpt type="module"> 的代码


六、关于运算符

1、运算符顺序

大部分运算符都是从左到右的运算,但也有特例:
①a = b = c,相当于 a = (b = c)
②a ** b ** c,相当于 a ** (b ** c)

2、关于运算符的有趣现象

(1, 2, 3) 返回3,因为逗号运算符取最后一个
(1, 2, 3, alert)(1) 弹框alert(1),先是返回 alert 函数,然后1是调用的参数
[1, 2, 3][1] 返回2,[1, 2, 3]是数组,[1]是下标
[1, 2, 3][1, 2] 返回3,[1, 2, 3][(1,2)],所以是 [1, 2, 3][2]


七、ES6模块和CommonJS的区别

ES6模块输出值的引用,CommonJS输出值的副本(也就是说用CommonJS的 require 引用的值,如果不是 getter 函数就会永远不变,而ES6 import 的就只要原始的变了,这个就会变)
ES6模块编译时输出接口,CommonJS运行时加载(因为CommonJS加载的是通过 module.exports 加载对象,对象只有执行后才能生成,而ES6只是静态定义,在编译的时候就能得到)


八、JSON对象

1)使用 JSON.parse() 把JSON对象转为对象
2)使用 JSON.stringify() 把对象转为JSON对象
深克隆的一种方法:JSON.parse(JSON.stringfy(obj))


九、函数防抖和函数节流

1、函数防抖

我们事件绑定后,只要一触发这个事件,就会马上调用这个回调函数
函数防抖,就是事件触发后,绑定的回调函数马上开始计时,只要在这段时间内,没有再次触发这个事件,时间一到就触发这个函数内的异步操作,如果时间没到,但是又触发了事件,就要重新计时

	function debounce(fun, time) {
		// 为什么这里要return function
		// 因为我们绑定的时候通过DOM0也就是dom.onclick = debounce(fun, 2)
		// 这样就会直接运行debounce函数,而点击事件调用时就没办法调用
		// 所以返回function,这样调用就相当于debounce(fun,time)()
		let timeout = null;
		return function() {
			// 再次调用要清空
			clearTimeout(timeout)
			timeout = setTimeout(fun, time)
		}
	}
2、函数节流

相当于在一段时间内只允许有一个被触发,其他的就算触发了,但是因为在第一个的时间过程内,就没有效果

	function throttle(fun, time) {
		let timeout = null;
		return function() {
			// 再次调用要清空
			if(!timeout) {
				timeout = setTimeout(() => {
					fun()
					timeout = null;
				}, time)
			}
		}
	}

十、作用域和执行上下文

1、作用域

作用域就相当于结界,自己作用域内的变量只能自己和自己的子孙作用域们能够拿到,兄弟和父母是无法拿到的
作用域一般有全局作用域、块级作用域(因为ES6的let和const实现)、函数作用域

我们经常通过立即执行函数 (function(){})() 来防止污染全局等作用域

当前作用域中没定义的变量叫自由变量(不带this),如果在当前作用域中找不到就根据作用域链去找
具体找法:
①看调用这个自由变量所在的作用域中有没有
②没有,则看这个作用域被定义不是调用)的父级作用域是谁,去这个上面找
③循环,直到找到为止

举个例子:

	let a = 1;
	function b() {
		console.log(a)   //结果是1
	}
	function c() {
		let a = 2
		b()
	}
	c()
2、执行上下文

引擎在预编译阶段(这是第二阶段,具体顺序是词法阶段->预编译阶段->执行阶段)创建第一个执行上下文,并压入函数调用栈(或者叫做执行上下文栈)的底部
之后的执行上下文只有执行才能知道是谁,然后入栈
执行上下文包括三个部分:作用域链变量对象(AO)this
所以我们可以看出 this 和执行上下文有那么点关系
但是 this 不一定就指向执行的那个对象
① this 指向 window
对于 setTimeoutsetInterval 只要没使用其他改变 this 指向方向的,就会指向window

② this 指向对象
对于 new 出来的对象,或使用 obj.a() 在 a 函数中打印 this,就会是 obj

	var x = 3
	function a() {
		let x = 1
		console.log(x)   // 1
		console.log(this.x)  // 2
	}
	var b = {
		x: 2
	}
	b.c = a
	b.c()


	
	var x = 3
	function a() {
		let x = 1;
		console.log(x)  // 1
		console.log(this.x)  // 3
	}
	var b = {
		x: 2
	}
	a()

③ this 指向自己绑定的
call、bind、apply 的共同点:
a 如果为 null 或 undefined,则 this 指向 window
a 如果是数字,字符串等基本类型,就指向它们的包装类型,如 Number、String 等

1)使用 Function.prototype.call(a, arg1, arg2)
立即执行,函数参数是多参

	Function.prototype.myCall = function(context, ...args) {
		// 因为是调用的Function实例下面的方法,所以这个时候这个this还是指向该function
		let fn = this;
		switch(typeof context) {
			case 'object':
				if(context !== null) {
					break;
				}
			case 'undefined':
				context = window;
				break;
			case 'string':
				context = String;
				break;
			case 'number':
				context = Number;
				break;
			case 'boolean':
				context = Boolean;
				break;
		}
		// 在要指向的this上添加一个属性,这样就相当于调用对象的this
		context.fn = fn
		// 调用即可
		let res = context.fn(args)
		// 删掉这个属性,免得把函数污染了
		delete context.fn
		return res
	}

2)使用 Function.prototype.bind(a, arg1, arg2)
返回新函数,bind函数参数是多参
这个其实也是跟call差不多,就是return的时候用function包装一下,而且考虑到方便,我这里引用了apply(毕竟参数这样传着方便)

	Function.prototype.myBind = function(context, ...args) {
		// 因为是调用的Function实例下面的方法,所以这个时候这个this还是指向该function
		let fn = this;
		return function() {
			fn.apply(context, args)
		}
	}

3)使用 Function.prototype.apply(a, [arg1, arg2])
立即执行,函数参数是数组或伪数组
因为跟call也就参数上不同,所以这里代码就只变动了参数

	Function.prototype.myApply = function(context, args) {
		// 因为是调用的Function实例下面的方法,所以这个时候这个this还是指向该function
		let fn = this;
		// 为什么这里不直接用obj,因为如果碰到NaN、0、‘’,这些的boolean也是false
		// 但是NaN和0的this是Number,‘’是String
		switch(typeof context) {
			case 'object':
				if(context !== null) {
					break;
				}
			case 'undefined':
				context = window;
				break;
			case 'string':
				context = String;
				break;
			case 'number':
				context = Number;
				break;
			case 'boolean':
				context = Boolean;
				break;
		}
		// 在要指向的this上添加一个属性,这样就相当于调用对象的this
		context.fn = fn
		// 调用即可
		let res = context.fn(args)
		// 删掉这个属性,免得把函数污染了
		delete context.fn
		return res
	}

④ 箭头函数的 this
指向定义时的外层函数

	var x = 2
	var a = {
		x: 1,
		b: function() {
			var x = 3
			console.log(this.x)  // 1
		}
	}
	a.b()

	var x = 2
	var a = {
		x: 1,
		b: () => {
			var x = 3
			console.log(this.x)  // 2
		}
	}
	a.b()

	var x = 2
	var a = {
		x: 1,
		b: function() {
			var x = 3
			setTimeout(() => {
				var x = 4
				console.log(this.x)  // 1
			}, 1000)
		}
	}
	a.b()

	var x = 1
	var a = {
		x: 2,
		b: {
			x: 3,
			c: {
				x: 4,
				e: function() {
					console.log(this.x)  // 4
				},
				d: () => {
					console.log(this.x)   // 1
				}
			}
		}
	}
	a.b.c.e()	  //普通函数就是嵌套的倒数第二个对象
	a.b.c.d()    //嵌套对象的箭头函数指向最顶层的function的

箭头函数的this不能通过call、bind、apply去改变this指向,毕竟他自己没有this,使用的this是它碰到的第一个父级函数(因为对象等是没有this的,只有函数有,精确点就是只有执行上下文有)的this


十一、深克隆和浅克隆

1、浅克隆

就是只会克隆最外面那一层,深层的还是通过引用指向同一块区域
实现方法:
①Array.prototype.concat()
②Array.prototype.slice()
③[…args]
④for循环一层
⑤Object.assign({}, obj) // 把obj复制给{}

2、深克隆

简单版本就是 const a = JSON.parse(JSON.stringify(b))
但这个有缺点:
①无法对 function 或 RegExp 或稀疏数组(也就是new Array(2)这种的)等特殊对象克隆
②会丢掉对象的 constructor,所有的构造函数指向 Object
③碰到对象循环引用就报错

所以完整版本就是

	function deepClone(arg, res=null) {
		let type = Object.prototype.toString.call(arg).slice(8, -1).toLowerCase()
		switch(type) {
			// 其实只需要考虑object、array和date,其他的都可以直接返回(因为是值引用)
			// 没做function的深拷贝,实在不会
			case 'object':
			case 'array':
				res = type === 'array' ? [] : {}
				let keys = Object.keys(arg)
				// 这里使用Object.keys而不使用for...in是因为
				// for...in走原型链,Object.keys不走
				for(let key of keys) {
					res[key] = deepClone(arg[key], res[key])
				}
				break;	
			case 'date':
				return new Date(arg.toString());
			default:
				return arg;
		}
		return res;
	}

十二、JS实现设计模式

1)观察者模式

这个其实在vue框架的数据绑定中有用到,这里先写一个最简单的观察者模式

	// 定义订阅者
	let subscriber= {
		// 放置有哪些观察者(之后会在触发某些事件或数据修改时,去广播自己存储的观察者)
		// 这些观察者都是要执行的方法
		ovservers: [],

		// 添加观察者
		addObserver: function(fn) {
			// 不能重复订阅!
			if(!this.ovservers.includes(fn)) {
				this.ovservers.push(fn)
			}
		},

		// 去掉观察者
		removeObserver: function(fn) {
			// 必须是有才能取消订阅
			if(this.ovservers.includes(fn)) {
				this.ovservers= this.ovservers.filter(item => {
					return item !== fn
				})
			}
		},

		// 通知
		notify: function(...arg) {
			this.ovservers.forEach(item => {
				item(...arg)
			})
		}
	}

观察者模式就相当于我(观察者 observer)校招,把简历投给A公司(订阅者 subscriber),A公司同时也收到了很份简历,他会在该职业有空位的情况下通知我们去面试
整个过程就只有订阅者观察者

这里就要提一下发布/订阅模式
网上有说观察者模式就是发布/订阅模式的,在GOF设计模式(四人帮)和head first 设计模式中给的例子其实是完完全全没有第三者的,而且根据维基百科可以看出观察者模式是25种设计模式之一,而发布/订阅模式是15种架构模式之一,设计模式,架构模式,并行模式,其他模式合称为软件设计模式,所以说狭义的设计模式中,观察者模式不等于发布/订阅模式,广义的设计模式中等于

所以在本文章中,把观察者模式和发布/订阅模式当不同的概念(其实可以不用那么去死扣这个,毕竟设计模式是哪个方便就用哪个)

发布/订阅模式就相当于我(订阅者 subscriber)校招,把简历投给第三方猎头(事件中心 eventemitter),第三方和公司(发布者 publisher)是有交流的,当公司有空位,就会告诉第三方,第三方会通知我们这些投了简历的
整个过程就有订阅者发布者事件中心
在发布/订阅模式中,订阅者是不知道发布者的
在这里插入图片描述
eventemitter:我这里的eventemitter用到的是发布/订阅模式(引入了一个事件中心,发布者只看是否触发事件,触发了就告诉事件中心,让事件中心去调用)(其实也可以拿观察者模式来实现,纯粹看自己要不要加入第三方,要不要低耦合)
其中的 on 就是添加订阅者到事件中心
off 去掉订阅者
emit 事件中心通知
once 添加订阅后,只要通知了一次,就把他从事件中心中去掉

	let publisher = {}

	let eventemitter = {
		// 因为这个on就跟dom2的addEventListener一样,所以说只有如果也有绑定的不会被覆盖
		on: function(event, fn) {
			if(!publisher[event]) {
				publisher[event] = []
			}
			publisher[event].push(fn)
		},

		off: function(event, fn) {
			if(publisher[event]) {
				// 如果是删除的整个事件,即不传第二个参数,就把所有都删了
				if(arguments.length === 1) {
					delete publisher[event]
					console.log('-----',publisher)
				} else {
					publisher[event] = publisher[event].filter(item => item !== fn)
					if(publisher[event].length === 0) {
						delete publisher[event]
					}
				}
			}
		},

		// 通知
		emit: function(event, ...arg) {
			if(publisher[event]) {
				publisher[event].forEach(fn => {
					fn(...arg)
				})
			}
		},

		once: function(event, fn) {
			// 因为once是属于先注册进去只要使用一次就会被清除的,所以可以把这个使用和清除包装成一个函数,到时候让emit调用即可
			let that = this
			let newFn = function(...arg) {
				// 使用
				fn(...arg)
				// 清除(这里要清除的是这个包装的函数)
				that.off(event, newFn)
			}
			// 注册
			this.on(event, newFn)
		}
	}

	eventemitter.on('normal1', (arg1, arg2) => { console.log(`normal event emitted ${arg1}, ${arg2}`); });
	eventemitter.once('normal2', () => { console.log('normal event emitted'); });
	eventemitter.once('once', () => { console.log('once event emitted'); });
	eventemitter.emit('normal1', 'hello', 'world'); // normal event emitted hello, world
	eventemitter.emit('once'); // once event emitted
	eventemitter.emit('once'); // 不打印
	eventemitter.off('normal1');
	eventemitter.emit('normal1'); // 不打印
	eventemitter.off();
	eventemitter.emit('normal2'); // normal event emitted
2)

十三、大文件上传

首先先谈谈普通文件上传的几种方式:
1)使用表单直接上传
2)如果是图片先使用base64编码(也可以使用二进制),然后在服务器端使用base64解码(这样的话有可能编码后的文件比原文件还大)
3)通过FormData对象,发送XHR来传文件,服务器端操作与使用表单一样
另外,FormData对象会将数据编译成键值对

	let formData = new FormData()
	formData.append('userName', 'aaa')
	let content = '<a id="a">1111</a>'
	let blob = new Blob([content], {type: 'text/xml'})
	formData.append('file', blob)
	let xhr = new XMLHttpRequest()
	xhr.open('POST', 'http://www.aaa.com/aaa?')
	xhr.send(formData)

其中FormData里的数据类型可以是blob、file、String

Blob 是一个不可变、


十四、class和普通构造函数的区别

相同点
1、class 上的自定义方法和普通构造函数上的方法一样都是放在prototype上的
2、class 和普通构造函数都可以使用new来创建对象(但是class必须使用new来创建,直接调用class是不行的,而普通构造函数可以像函数一样调用
3、如果class没有父类,那么它的__proto__和普通函数的__proto__一样,是Function.prototype,又因为Function.prototype === Function.__proto__ 所以class的__proto__也等于Function.__proto__,当然,如果有继承关系,那么原型肯定不是这样的了

不同点
1、class必须使用new调用,普通构造函数可以当做普通函数调用
2、class如果有继承,那么必须先在构造函数中使用super(),才能使用this,因为class中的this其实是改写的父类中的this,而普通构造函数的继承是现有子类,然后将父类方法添加到子类上(person.apply(this))这样

如果class不是继承(也就是说没有使用extends)就可以在没有写super()的情况下使用this

3、class和let、const一样,不能重复声明且不能变量提升,而function可以提升
4、虽然在以前可以使用原型链来做继承,但是这种情况下的继承是先有的子类,然后在子类上添上父类的属性(相当于拷贝),并没有切实地使用父类的属性,但是class因为是先有的父类,然后在父类的基础上改,所以就能拿到内部属性
在这里插入图片描述
在这里插入图片描述


十五、位运算

1、二进制表示

之前有讲到,在ES中数值都是用IEE754 64位存储的,但是位操作符不是直接操作64位的值,而是用的32位带符号的整数进行计算的,返回值也是一个32位带符号的数
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
二进制一般只展示有效位,如果前面都为0就可以忽略不计

2、负数的二进制及补码的求解方式

负数的带符号二进制数通过先求其绝对值的二进制然后得到反码再加一,这个过程也是求补码的过程
但是ES会自己简写(就用负号加上有效位),如下所示

	let a = -18
	console.log(a.toString(2)) // '-10010'
3、7种位运算符

总共7种位运算符:按位非(~)、按位与(&)、按位或(|)、按位异或()、左移、有符号右移、无符号右移

①按位非(~)

按位非就是求反码,对于十进制来讲就是操作数取负再减一
如果连续用两次按位非,就可以得到原来的值,如果对小数用两次按位非就可以得到该值的整数形式(跟Math.floor()差不多)

	console.log(~1) // -2
	console.log(~~1) // 1
	console.log(~-1) // 0
	console.log(~1.1) // -2
	console.log(~~1.1) // 1
	console.log(~~1.9) // 1 

②按位与(&)

这个按位与和逻辑与(&&)长得很像就是符号是一个还是两个问题,但是其中的操作也是不一样的。
首先,按位与是把两个数的每一位都进行相与,如果两个数的那一位是两个1,结果就是1,否则都为0,除此之外,按位与不会短路,最后的结果是一个实实在在的值
逻辑与是对两个数进行判断,如果第一个就为假,那么就短路,并把第一个值输出(js中是直接输出,不会转换成false),如果两个都为真,就输出最后一个值


③按位或(|)

这个也跟逻辑或(||)差不多,但是他们也是两码事
按位或会把每一位都进行或运算,如果全为0,才为0,否则就是1,不会短路
逻辑或也会短路,如果第一个是真的,就输出第一个,如果第一个是假的,输出第二个

其中有两个小技巧,整数和0按位或可以得到整数本身,小数和0按位或可以取整(和两个按位非结果相同)


④按位异或(^)

按位异或就是每一位,相反为1,否则为0
按位异或也有几个小技巧,就是两个数交换,对两个数进行三次按位异或就可以达到交换的效果,如下所示

	a ^= b
	b ^= a
	a ^= b

这样就可以在不引入变量的情况下交换两个数
除此之外,整数和0按位异或可以得到自身,小数和0按位异或可以取整(跟按位或一样的)


⑤左移(<<)

左移就相当于把有效数位往左移动,然后后面补0
2 << 3就相当于 2 * (2 ** 3)a << b就相当于a * (2 ** b)
左移不会影响正负号,也就是说 -2 << 3 还是-16
另外,左移0位,可以取整(跟按位异或一样)
左移只能用两个小于号,没有三个小于号的


⑥有符号右移(>>)

有符号右移不会更改操作数的负数,a >> b相当于 a / (2 ** b)
这个有符号右移0位也能取整
有符号右移是两个大于号!!!


⑦无符号右移(>>>)

首先,无符号右移是三个大于号!!!
因为无符号右移总是会拿0填充左侧,哪怕右移0位,也会变正,所以无符号右移的结果没有负数!!
所以,如果是正数,那么有符号右移和无符号右移结果相同,但如果是负数,就一般都不同,如-2 >>> 2结果是1073741823,再看一个负数右移0位-2 >>> 0结果是4294967294

4、总结

我们通常会把按位运算用在取整交换乘法除法的方面
取整
~~:两个按位非
|:按位或
^:按位异或
<<0:左移0位
>>0:有符号右移0位也能取整

交换
使用按位异或

	a ^= b
	b ^= a
	a ^= b

乘法
使用有符号左移<<

除法
使用有符号右移.>>
如果操作数为正数,也可以使用无符号右移>>>


十六、==比较

在这里插入图片描述

十七、js八大继承(参考js高程+ES6)

在这里插入图片描述

1、使用原型链继承
	function Parent() {}
	function Children() {}
	Children.prototype = new Parent()
	// 原型的构造函数要指向自己
	Children.prototype.constructor = Children

缺点:
1、由于子类通过prototype来得到父类的一个实例,当父类的属性是引用属性的时候,就可能被子类修改
2、调用子类构造函数时,不能向父类的构造函数传递参数


2、构造函数继承
	function Parent() {}
	function Children(arg) {
		Parent.call(this, arg)
	}

本质就是通过call把父类的属性和方法都copy到子类实例上面
缺点:
1、每个子类实例都有父类实例的一个副本,影响性能(也就是说方法不能复用)
2、无法访问父类原型上的属性和方法,只能拿到实例上的方法


3、组合继承
	function Parent() {}
	function Children(arg) {
		Parent.call(this, arg)
	}
	Children.prototype = new Parent()
	Children.prototype.constructor = Children

在这里插入图片描述
缺点:
1、原型上会有两份相同的属性和方法,太占空间


4、原型式继承

这个跟第一种的原型链继承就差一个字,实际上区别也不是很大,但是这个对于寄生以及寄生组合继承就是很有用的
这个其实可以通过ES5提供的Object.create()来做到
也可以自己写一个函数,利用一个空对象做中介,将父属性赋值到到空对象的原型上,然后返回该对象的实例

	function createObjefct(obj) {
		function f() {}
		f.prototype = obj
		return new f()
	}
	function Parent() {}
	let c = createObject(Parent)

缺点:(跟原型链是一样的)
1、因为这是浅复制,父类原型上的引用属性可以被篡改
2、无法传递给父类构造函数参数


5、寄生式继承

原型式继承上再次更改

	function createObject(obj) {
		function f() {}
		f.prototype = obj
		return new f()
	}
	function createAnother(obj) {
		let clone = createObject(obj)
		// 以某种方式增强对象
		clone.sayHi = function(){}
		return clone
	}
	function parent() {}
	let c = createAnother(parent)	

缺点:
1、每次都会给子类新建一个方法,方法不能复用,从而很占空间(跟构造函数继承的第一点一样)
2、父类原型上的引用属性可能被修改


6、寄生组合式继承

通过构造函数继承属性,通过原型链继承方法

	function createObject(obj) {
		function f() {}
		f.prototype = obj
		return new f()
	}
	function createAnother(parent, children) {
		let p = createObject(parent)
		p.constructor = children
		children.prototype = p
	}
	function parent() {}
	function children() {}
	createAnother(parent, childrent) 

缺点:
1、子类添加原型方法只能通过prototype.xxx来添加如果直接使prototype=xxx会覆盖父类原型继承的对象


7、es6的extends

通过babel转换成es5后发现,这其实就是寄生组合式的语法糖
****在这里插入图片描述


8、混入式继承

将父类的属性和子类的属性都复制到另一个新的属性上,或把父类的属性复制到子类上
通过**Object.assign()**这是浅拷贝

	function Parent() {}
	function Children() {}
	let f = Object({}, Parent.prototype, Children.prototype)
	let c = new f()

十八、手写new
	function myNew(constructor, ...arg) {
		// 设置对象的__proto__
		// 原型式继承
		let obj = Object.create(constructor.prototype)
		// 将构造函数上的属性和方法绑定到对象上
		// 构造方法继承
		let res = constructor.call(obj, ...arg)
		return res instanceof Object ? res : obj
	}

十九、手写promise一家
	const STATUS = {
		PENDING: 'pending',
		FULFILLED: 'fulfilled',
		REJECTED: 'rejected'
	}
	class myPromise {
		constructor(executor) {
			// promise初始状态
			this.status = STATUS.PENDING
			// then 回调值
			this.value = undefined
			this.resolveQueue = []
			this.rejectQueue = []

			// 箭头函数固定this
			let resolve = value => {
				const run = () => {
					// 只有当前状态为pending才能调用resolve
					if(this.status === STATUS.PENDING) {
						this.status = STATUS.FULFILLED
						this.value = value

						// 执行resolve回调(也就是then里的东西)
						while(this.resolveQueue.length !== 0) {
							let callback = this.resolveQueue.shift()
							callback(value)
						}
					}
				}
				setTimeout(run)
			}

			let reject = value => {
				const run = () => {
					if(this.status === STATUS.PENDING) {
						this.status = STATUS.REJECTED
						this.value = value

						if(this.rejectQueue.length !== 0) {
							let callback = this.rejectQueue.shift()
							callback(value)
						}
					}
				}
				setTimeout(run)
			}
			executor(resolve, reject)
		}

		then(onFulfilled, onRejected) {
			return new myPromise((resolve, rejected) => {
				let resolveFn = value =>{
					try {
						let x = onFulfilled(value)
						x instanceof myPromise ? x.then(resolv, reject) : resolve(x)
					} catch (error) {
						rejected(error)
					}
				}

				let rejectFn = value => {
					try {
						let x = onRejected(x)
						x instanceof myPromise ? x.then(resolve, reject) : rejected(x)						
					} catch (error) {
						reject(error)
					}

				}

				switch(this.status) {
					case STATUS.PENDING:
						this.resolveQueue.push(resolveFn)
						this.rejectQueue.push(rejectFn)
						break
					case STATUS.FULFILLED:
						resolveFn(this.value)
						break
					case STATUS.REJECTED:
						rejectFn(this.value)
				}
			})
		}

		catch(reject) {
			return this.then(undefined, reject)
		}

		finally(callback) {
			return this.then(value => myPromise.resolve(callback()).then(() => value), error = )
		}

		static resolve(fn) {
			return fn instanceof myPromise ? value : new myPromise(resolve => resolve(fn))
		}

		static reject(fn) {
			return fn instanceof myPromise ? value : new myPromise((resolve, reject) => reject(fn))
		}

		static all(arg) {
			arg = Array.from(arg)
			let count = 0
			let len = arg.length
			let result = []
			return new myPromise((resolve, reject) => {
				arg.forEach((item, index) => {
					myPromise.resolve(item).then(res => {
						count++
						result[index] = res
						if(count === len) {
							resolve(result)
						}
					}).catch(err => {
						reject(err)
					})
				})
			})
		}

		static race(arr) {
			arr = Array.from(arr)
			return new myPromise((resolve, reject) => {
				arr.forEach(item => {
					myPromise.resolve(item).then(res => {
						resolve(res)
					}).catch(err => {
						reject(err)
					})
				})
			})
		}
	}

十九、Object.defineProperty

配置项:
1、get
2、set
3、configurable(默认是false
主要就是是否能通过delete把某个属性删除
当然,如果直接通过var a ={c: 1, d: 2}然后delete a.c是可以正常删除的(因为这个时候的configurable应当是true)
4、enumerable(默认是false
是否可枚举
当然,如果是普通对象也是不可枚举的
5、writable(默认是false
是否可以赋值
对于数组来说,push和unshift这种没影响
但如果是对象就不行


二十、手写instanceof

	function myInstanceOf(a, b) {
		let proto = a.__proto__, prototype = b.prototype
		while(proto !== null && proto !== undefined) {
			if(proto === prototype) {
				return true
			} else {
				proto = proto.__proto__
			}
		}
		return false
	}

二十一、手写ajax

	function myAjax(url) {
			let xhr = new XMLHttpRequest()
			xhr.open('GET', url)
			xhr.onreadystatechange = function() {
				// readyState为0表示请求未初始化
				// 1表示连接建立(open已调用)
				// 2表示请求接收(send调用)
				// 3表示请求处理中
				// 4表示请求完成
				if(xhr.readyState === 4) {
					if(xhr.state === 200 || xhr.state === 304) {
						console.log(xhr.responseText)
					}
				}
			}
			xhr.send()
		}

二十二、函数柯里化

	function curry(fn) {
		let args = []
		function temp(...newArg) {
			if(newArg.length !== 0) {
				args = [
					...args,
					...newArg
				]
				return temp
			} else {
				let val = fn.apply(this, args)
				args = []
				return val
			}
		}
		return temp
	}
	function add(...args) {
		return args.reduce((a, b) => a + b)
	}
	let addCurry = curry(add)
	console.log(addCurry())

二十三、Array.prototype.some和Array.prototype.every

some是遍历每一项,有一个为true就返回true,every是遍历每一项,每一个为true就返回true,否则false

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值