javascript
1、数据类型U SO NB
- 基本数据类型:undifine,String,Symbol,number,null,Boolean
- 引用数据类型:objerct
NaN和任何值包括本身都不相等
Symbol的作用: 本质上是一种唯一标识符,可用作对象的唯一属性名,这样其他人就不会改写或覆盖你设置的属性值
- 唯一性,用同一个变量生成的值也不一样
- 隐藏性:for···in,object.keys() 不能访问(但Object.getOwnPropertySymbols)
全局注册登记:Symbol.for()
通过symbol对象获取到参数值:Symbol.keyFor()
2、判断变量类型
typeOf
用于判断基础类型,如果判断引用类型,除函数外只会返回object
typeOf null 会返回object js用低位储存变量存储变量信息,object为000开头,而null表示为全0,因此会错误判断
instanceOf
原理是基于instanceOf的查询,只要判断的对象处于原型链中,就判断为true
判断基本类型:可以使用Symbol.hasInstance 重新定义InstanceOf
实现instanceOf
function myInstanceof(left, right) {
//基本数据类型直接返回false
if(typeof left !== 'object' || left === null) return false;
//getProtypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
let proto = Object.getPrototypeOf(left);
while(true) {
//查找到尽头,还没找到
if(proto == null) return false;
//找到相同的原型对象
if(proto == right.prototype) return true;
proto = Object.getPrototypeof(proto);
}
}
console.log(myInstanceof("111", String)); //false
console.log(myInstanceof(new String("111"), String));//true
Object.toString().call及其原理
Object.toString()//"function Object() { [native code] }"
Object.prototype.toString()//"[object Object]"
不同的数据类型都有其自身toString()方法
所有类都继承于Object,因此toString()方法应该也被继承了
Object,Object.prototype上都有toString()方法
所有类在继承Object的时候,改写了toString()方法。 Object原型上的方法是可以输出数据类型的。因此我们想判断数据类型时,也只能使用原始方法。继而有了此方法:Object.prototype.toString.call(obj)
call则是用来改变this的走向
3、数据类型转换
相等 == 和===
===叫做严格相等,是指:左右两边不仅值要相等,类型也要相等,例如’1’===1的结果是false,因为一边是string,另一边是number。
转换流程:
- 如果Symbol.toPrimitive()方法,优先调用再返回
- 调用valueOf(),如果转换为原始类型,则返回
- 调用toString(),如果转换为原始类型,则返回
- 如果都没有返回原始类型,会报错
== 不像 === 那样严格,对于一般情况,只要值相等,就返回true,但==还涉及一些类型转换,它的转换规则如下:
- 两边的类型是否相同,相同的话就比较值的大小,例如1==2,返回false
- 判断的是否是null和undefined,是的话就返回true
- 判断的类型是否是String和Number,是的话,把String类型转换成Number,再进行比较
- 判断其中一方是否是Boolean,是的话就把Boolean转换成Number,再进行比较
- 如果其中一方为Object,且另一方为String、Number或者Symbol,会将Object转换成字符串,再进行比较
强制转换和隐式转换
强制转换:通过String(),Number(),Boolean()函数强制转换
隐式转换:
1.undefined等于null
2.字符串和数字比较时,字符串转数字
3.数字为布尔比较时,布尔转数字
4.字符串和布尔比较时,两者转数字
5.字符串加数字,数字就会转成字符串
6.数字减/乘/除字符串,字符串转成数字
包装类型
String,Nubmber,Boolean
些类型和其他引用类型相似,但同时 也具备 与各自基本类型相应的特殊行为。 实际上:每当读取一个基本类型值的时候, “后台就会创建一个 对应的基本包装类型的对象”,从能能够调用一些方法来操作这些数据
String
不是引用类型,但用new创建的String对象就是引用类型
Boolean
无内置方法
Number
4、闭包
-
概念:闭包是指有权访问另外一个函数作用域中的变量的函数
-
闭包有三个特性:
1.函数嵌套函数
2.函数内部可以引用外部的参数和变量
3.参数和变量不会被垃圾回收机制回收 -
产生原因:
在ES5中只存在两种作用域————全局作用域和函数作用域,
当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链
,值得注意的是,每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。闭包产生的本质就是,当前环境中存在指向父级作用域的引用 -
作用:设计私有方法和变量
-
优点:
1、可以读取函数内部的变量,
2、让这些变量的值始终保持在内存中,并不会在一个函数调用后被清除,3、可以避免全局变量的污染
-
缺点:
1、闭包会使得函数中的变量都被保存在内存中,消耗很大,退出函数时要将用不到的变量清除 -
表现形式
-
返回一个函数
-
作为函数参数传递
var a = 1; function foo(){ var a = 2; function baz(){ console.log(a); } bar(baz); } function bar(fn){ // 这就是闭包 fn(); } // 输出2,而不是1 foo();
-
在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。
-
5、作用域
作用域:一个变量的作用范围
1、全局作用域:
(1)、在页面打开时产生,关闭时销毁
(2)、在scrpt标签中编写的变量和函数作用域为全局,任何地方可以访问到
(3)、windows为全局对象,代表一个浏览器窗口
(4)、全局作用域中声明的函数作用域为全局作用域
2、函数作用域:
(1)、调用函数时产生,执行结束销毁
(2)、每调用一次会产生一个作用域
(3)、函数作用域可以访问全局作用域,函数外部无法访问函数作用域内的变量
(4)、在访问变量时,在自身作用中查找,找不到则到上一级作用域中寻找
- 作用域的深层次理解:
执行器的上下文:
-当函数执行前期会创建一个执行期上下文的内部对象AO(函数作用域)
-这个内部的对象是预编译时创建的,函数被调用时会先进行预编译
-在全局代码的执行前期会创建一个执行期的上下文对象GO - 再理解什么是作用域链:
作用域链:保存在隐式的属性scope中,用于给js引擎访问,里面存储着作用域链,是AO和GO的集合
当一个函数内部的变量被其他函数访问是会产生一个作用域链,连接着函数与变量,只有当变量没有被任何函数访问是才会被回收
6、原型和原型链
1、原型对象和构造函数的关系
-
在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会天生自带一个prototype属性,这个属性指向函数的原型对象。
-
当函数经过new调用时,这个函数就成为了构造函数,返回一个全新的实例对象,这个实例对象有一个__proto__属性,指向构造函数的原型对象。
-
原型对象上也有一个属性constructor,指向构造函数
2、原型链:如果原型对象也是另一个原型对象的实例,那么同样也会有指向原型对象的原型的指针,这样层层递进就构成了实例和原型的链条,也就是原型链了
原型链最终会指向对象的原型(Object.prototype),而对象的原型则指向null,但Object本身也都是函数的,所以他上面都会有一个指针(_ _ proto _)指向函数的原型(Function()),而函数的原型最终也会指向对象的原型,一般情况下,构造函数的隐式原型和显式原型并不相等。但有一个例外,Function 的隐式原型和显式原型相等。Function.prototype = Function.__proto__
3、作用:是实现继承的主要方法
继承的基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法(使一个引用类型的原型指针指向另一个引用类型)
7、继承
1、借助原型链
Child.prototype = new Parent() // 关键
Child.prototype.constructor = Child // 让子类型的原型的constructor指向子类型,否则它指向的是Parent
缺点:原型对象都是共用的
2、借助call(构造函数)
function Parent1(){
this.name = 'parent1';
}
function Child1(){
Parent1.call(this);//改变this
this.type = 'child1'
}
console.log(new Child1);
优点:可以向父类传递参数,而且解决了原型链继承中父类属性使用 this 声明的引用类型属性会在所有实例共享的问题。
缺点:只能解决父类型上的属性和方法的继承,但是父类型原型上的不能继承
3、寄生组合继承
function Parent5 () {
this.name = 'parent5';
this.play = [1, 2, 3];
}
function Child5() {
Parent5.call(this);
this.type = 'child5';
}
//使用create()
Child5.prototype = Object.create(Parent5.prototype);
Child5.prototype.constructor = Child5;
Object.create的ES5写法
function create(o) {
function F() {} // 创建一个空的构造函数
F.prototype = o // 原型指向o
return new F() // 返回的是new构造函数的实例对象
}
面向对象的设计一定是好的设计吗?
不一定。从继承的角度说,这一设计是存在巨大隐患的。
继承的最大问题在于:无法决定继承哪些属性,所有属性都得继承。
面向组合就是先设计一系列零件,然后将这些零件进行拼装,来形成不同的实例或者类。
8、call/apply/bind
apply和call共同点:改变函数的执行时的上下文,能劫持另外一个对象的方法,继承另外一个对象的属性
区别:apply:,第二个参数是一个数组或类数组 Function.apply(obj,[param1,param2])(什么是类数组:类数组无法使用 forEach、splice、push 等数组原型链上的方法,毕竟它不是真正的数组。)
call:与apply一样,但参数是多 个 Function.call(obj,param1,param2)
call
call的用法:
//对象的继承
function superClass () {
this.a = 1;
this.print = function () {
console.log(this.a);
}
}
function subClass () {
superClass.call(this);
this.print();
}
subClass()
//借用方法
let domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));
call的实现:
//1、将函数设为对象的属性
//2 、执行该函数
//3、删除该函数
Function.prototype.call = function (context, ...args) {
//判断是否传入第一个参数
var context = context || window;
//获取调用call的函数,即this,将其设置为传入对象的属性
context.fn = this;
//ES3
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
//使用字符串方式将数组传参进入函数并执行,args会自动调用toString()方法,也就是变成了"arguments[1],arguments[2]"
var result = eval('context.fn(' + args +')');
//ES6用eval执行执行函数,并获取返回的结果
let result = eval('context.fn(...args)');
delete context.fn// 删除对象上的属性
return result;// 返回结果
}
apply
apply的用法:
//获取数组最大值
var max = Math.max.apply(null, array);
//合并两个数组
Array.prototype.push.apply(arr1, arr2);
apply的实现:
Function.prototype.apply = function (context, args) {
let context = context || window;
context.fn = this;
let result = eval('context.fn(...args)');
delete context.fn
return result;
}
bind
- bind():创建一个新函数,在调用时设置this为提供的值
- 对于普通函数,绑定this指向
- 对于构造函数,要保证原函数的原型对象上的属性不能丢失
与call(),apply()的区别:bind返回的是函数,需要稍后调用才会执行
(如果第一个参数为null则this指向window)
Function.prototype.bind= Function(context,...args){
//处理异常
if(typeOf this!=function){
throw new Error('Function.prototype.bind-what is trying to be bound is not callable')
}
//保存this的值,它代表调用bind的函数
var self = this
//定义一个函数
var fbound = function(){
//判断this是否指向实例,指向实例则为构造函数,将this绑定为实例对象,否则将绑定函数的 this 指向 context
self.apply(this instanceOf self)?
this:
context,args.concat(Array.prototype.slice.call(arguments))
}
//继承原型属性
fbound = Object.create(this.prototype)
//返回一个函数
return fbound
}
9、DOM事件流和事件委托(代理)
事件流
一个事件发生后会在子元素和父元素之间传播,分为3个阶段
1、从window对象传导到节点,称为捕获阶段,阻止:用preventDefault()
2、在目标节点上触发,称为“目标阶段”
3、从目标节点传导会window对象,称为冒泡阶段,阻止:用stopPropagation|stopImmediatePropagation
事件代理
事件代理:由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。
如果希望事件到某个节点为止,不再传播,可以使用事件对象的stopPropagation
方法,但只会阻止当前事件的传播,而不能阻止其他事件的传播(即后面有同类事件仍会传播),要彻底阻止所有同类事件的监听函数不再触发,要使用stopImmediatePropagation
-
优点:让节点的父级代为执行事件。而不需要循环遍历元素的子节点,大大减少dom操作
-
缺点:
1.不适应所有的事件,只适用于支持事件冒泡的事件
2.原理上执行就近委托
当然,为了防止父级点击事件也生效,需要判断event.target的值。
10、常见数组的方法
改变数组的方法:sort(),splice,pop(),push()
pop
Array.prototype.pop = function() {
let O = Object(this);
let len = this.length >>> 0;
if (len === 0) {
O.length = 0;
return undefined;
}
len --;
let value = O[len];
delete O[len];
O.length = len;
return value;
}
push
Array.prototype.push = function(...items) {
let O = Object(this);
let len = this.length >>> 0;
let argCount = items.length >>> 0;
// 2 ** 53 - 1 为JS能表示的最大正整数
if (len + argCount > 2 ** 53 - 1) {
throw new TypeError("The number of array is over the max value restricted!")
}
for(let i = 0; i < argCount; i++) {
O[len + i] = items[i];
}
let newLength = len + argCount;
O.length = newLength;
return newLength;
}
slice
slice() 方法可从已有的数组中返回选定的元素。不包括end
splice
splice() 方法向/从数组中添加/删除项目,然后返回被删除的项目,该方法会改变原始数组
concat
连接两个数组,返回一个新数组
filter
过滤:参数: 一个函数参数。这个函数接受一个默认参数,就是当前元素。这个作为参数的函数返回值为一个布尔类型,决定元素是否保留。
返回值为一个新的数组,这个数组里面包含参数里面所有被保留的项。
Array.prototype.filter = function(callbackfn, thisArg) {
// 处理数组类型异常
if (this === null || this === undefined) {
throw new TypeError("Cannot read property 'filter' of null or undefined");
}
// 处理回调类型异常
if (Object.prototype.toString.call(callbackfn) != "[object Function]") {
throw new TypeError(callbackfn + ' is not a function')
}
let O = Object(this);
let len = O.length >>> 0;
let resLen = 0;
let res = [];
for(let i = 0; i < len; i++) {
if (i in O) {
let element = O[i];
if (callbackfn.call(thisArg, O[i], i, O)) {
res[resLen++] = element;
}
}
}
return res;
}
map
创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果
Array.prototype.map = function(callbackFn, thisArg) {
// 处理数组类型异常
if (this === null || this === undefined) {
throw new TypeError("Cannot read property 'map' of null or undefined");
}
// 处理回调类型异常
if (Object.prototype.toString.call(callbackfn) != "[object Function]") {
throw new TypeError(callbackfn + ' is not a function')
}
// 草案中提到要先转换为对象
let O = Object(this);
let T = thisArg;
let len = O.length >>> 0;
let A = new Array(len);
for(let k = 0; k < len; k++) {
// 还记得原型链那一节提到的 in 吗?in 表示在原型链查找
// 如果用 hasOwnProperty 是有问题的,它只能找私有属性
if (k in O) {
let kValue = O[k];
// 依次传入this, 当前项,当前索引,整个数组
let mappedValue = callbackfn.call(T, KValue, k, O);
A[k] = mappedValue;
}
}
return A;
}
reduce
累加:接收两个参数,一个为回调函数,另一个为初始值。回调函数中三个默认参数,依次为积累值、当前值、整个数组,返回计算结果
Array.prototype.reduce = function(callbackfn, initialValue) {
// 异常处理,和 map 一样
// 处理数组类型异常
if (this === null || this === undefined) {
throw new TypeError("Cannot read property 'reduce' of null or undefined");
}
// 处理回调类型异常
if (Object.prototype.toString.call(callbackfn) != "[object Function]") {
throw new TypeError(callbackfn + ' is not a function')
}
let O = Object(this);
let len = O.length >>> 0;
let k = 0;
let accumulator = initialValue;
if (accumulator === undefined) {
for(; k < len ; k++) {
// 查找原型链
if (k in O) {
accumulator = O[k];
k++;
break;
}
}
// 循环结束还没退出,就表示数组全为空
throw new Error('Each element of the array is empty');
}
for(;k < len; k++) {
if (k in O) {
// 注意,核心!
accumulator = callbackfn.call(undefined, accumulator, O[k], O);
}
}
return accumulator;
}
sort
传入一个函数function(a,b)进行排序,当比较函数返回值大于0,则 a 在 b 的后面,即a的下标应该比b大。反之,则 a 在 b 的后面,即 a 的下标比 b 小。
不传则将数字转为字符串,按字母unicode值进行升序排序,
快速排序
数组扁平化flat
多维数组转一维数组
/* reduce 实现 */
function flat(arr) {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flat(cur) : cur)
}, [])//表示初始值
}
//只要有一个元素有数组,那么循环继续
while (ary.some(Array.isArray())) {
ary = [].concat(...ary);
}
数组去重
let arr1 = [1,2,3,4,4,1]
let newArr = arr1.reduce((pre,cur)=>{
if(!pre.includes(cur)){
return pre.concat(cur)
}else{
return pre
}
},[])
console.log(newArr);// [1, 2, 3, 4]
11、new对象时做了什么?
new
被调用后做了三件事情:
1、创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
2、属性和方法被加入到 this 引用的对象中。
3、新创建的对象由 this 所引用,并且最后隐式的返回 this 。
var obj = {};
obj.__proto__ = Base.prototype;
Base.call(obj);
function newFactory(ctor, ...args) {
if(typeof ctor !== 'function'){
throw 'newOperator function the first param must be a function';
}
let obj = new Object();
obj.__proto__ = Object.create(ctor.prototype);
let res = ctor.apply(obj, args);
let isObject = typeof res === 'object' && res !== null;
let isFunction = typoof res === 'function';
return isObect || isFunction ? res : obj;
};
12、防抖节流
防抖
// 一段时间内触发重新开始计时
function debounce(fn,delay){
let timer
return function(args){
clearTimeout(timer)
timer = setTimeout(function(){
fn(args)
},delay)
}
}
let inputFun = function(value){
console.log(value)
}
let input = document.getElementById('input')
let debounceInput = debounce(inputFun)
input.addEventListener('keyup',(e)=>{
debounceInput(e.target.value,10000)
})
节流
// 一段时间内只触发一次
function throttle(fun, wait) {
let timer
return function () {
if (!timer) {
timer = setTimeout(function () {
timer = null
fun()
}, wait)
}
}
}
function handle() {
console.log(Math.random())
}
// 直接等于throttle,不要再用函数封装
document.getElementById('button').onclick=throttle(handle, 1000)
13、requestAnimationFrame
html5 还提供一个专门用于请求动画的API,那就是 requestAnimationFrame,顾名思义就是请求动画帧。
- 优点
- 与setTimeout相比,requestAnimationFrame最大的优势是**由系统来决定回调函数的执行时机。**它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。
- CPU节能,页面处理不被激活则不刷新
- 函数节流:使用requestAnimationFrame可保证每个刷新间隔内,函数只被执行一次,
- 缺点
- 兼容问题
14、this的指向
this即执行上下文
1、全局上下文this指向window,严格模式指向undefined
2、函数调用时this指向调用函数的对象
3、对象.方法调用,指向对象
4、DOM事件绑定
onclick和addEventerListener中 this 默认指向绑定事件的元素。
IE比较奇异,使用attachEvent,里面的this默认指向window。
5、new+构造函数
此时构造函数中的this指向实例对象。
6、箭头函数
箭头函数没有this, 因此也不能绑定。里面的this会指向当前最近的非箭头函数的this,找不到就是window(严格模式是undefined)。
15、事件循环
(js为什么是单线程的) 如何保证代码的执行,宏任务和微任务
1、js为什么是单线程的
js是脚本语言,主要用来与用户互动,操作dom,如果是多线程的,有两个线程对同一个DOM进行添加和删除,这时候浏览器就不知道以哪个线程为准了
2、如何保证代码的执行
执行栈:js第一次执行时,js引擎会将其中的同步代码放到执行栈中从头顺序执行,通过执行的是一个方法则在执行栈中添加该方法的执行环境,在该环境执行代码,当该方法代码执行完了就会销毁该执行环境,回到上一级的执行环境
3、事件循环
js事件分为同步事件和异步事件
js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码...,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的原因。
4、js任务:
js任务分为同步和异步任务,同步是阻塞的,而异步是非阻塞的,
同步任务在主线程上执行,异步任务进入任务队列,当异步操作被触发时才放到主线程里面执行
异步任务又分为宏任务和微任务:
宏任务:setTimeout、setInterval、setImmediate、I/O、UI rendering
微任务:promise.then、process.nextTick、MutationObserver、queneMicrotask(开启一个微任务)
一个宏任务执行后会查看是否有微任务,有的话会先执行完所有微任务(期间有新微任务加入会一并执行),执行完后再继续执行下一个宏任务
4、使用微任务的原因:
* 减少操作中用户可感知到的延迟
* 确保任务顺序的一致性,即便当结果或数据是同步可用的
* 批量操作的优化
16、垃圾回收
内存限制
V8在64位系统下只能分配1.4G的内存
原因:js单线程,一旦进行垃圾回收,则其他运行逻辑要暂停
垃圾回收非常耗时间(对堆做一次小的垃圾回收要50ms以上)
新生代回收
V8 把堆内存分成了两部分进行处理——新生代内存和老生代内存,新生代就是临时分配的内存,存活时间短
Scavege算法
新生代内存空间分为正在使用的部分from和闲置的部分to
V8 将From部分的对象检查一遍,如果是存活对象那么复制到To内存中(在To内存中按照顺序从头放置的),如果是非存活对象直接回收即可。
当所有的From中的存活对象按照顺序进入到To内存之后,From 和 To 两者的角色对调,From现在被闲置,To为正在使用,如此循环。
Scavenge 算法主要就是解决内存碎片的问题,在进行一顿复制之后,闲置空间保证连续
缺点:只能用新生代内存的一般
老生代回收
新生代中的变量如果经过多次回收后依然存在,那么就会被放入到`老生代内存`中,这种现象就叫`晋升`。
发生晋升的情况:
已经经历过一次 Scavenge 回收。
To(闲置)空间的内存占用超过25%。
垃圾回收
1、标记回收:先遍历堆中所有对象,做上标记,然后对代码环境中使用的变量和被强引用的对象取消标记
2、删除掉未做标记的变量
3、空间碎片整理,在清除阶段结束后,把存活的对象全部往一端靠拢。
增量标记:由于回收耗时会阻塞任务进行,所以标记任务分多次,每做完一小部分后就让js执行一下,然后再继续
17、promise
就是一个对象,用来传递异步操作的消息。
Promise 对象代表一个异步操作,有三种状态:Pending
(进行中)、Resolved
(已完成,又称 Fulfilled)和 Rejected
(已失败)。
Promise 实例生成以后,可以用 then 方法和 catch 方法分别指定 resolved 状态和 rejected 状态的回调函数。
优点:
链式调用解决回调地狱
- 多层嵌套的问题。
- 每种任务的处理结果存在两种可能性(成功或失败),那么需要在每种任务执行结束后分别处理这两种可能性。
- 回调函数延迟绑定
- 返回值穿透
- 错误冒泡
缺点:对于长的链式操作来说,看起来是一堆then方法的堆砌,代码冗余,语义也不清楚,靠着箭头函数才使得代码略微简短一些。还有一个痛点,就是传递参数太麻烦,尤其是需要传递多参数的情况下
http://47.98.159.95/my_blog/blogs/javascript/js-async/006.html#%E7%AE%80%E6%98%93%E7%89%88%E5%AE%9E%E7%8E%B0
resolve
//传参为一个 Promise, 则直接返回它。
//传参为一个 thenable 对象,返回的 Promise 会跟随这个对象,采用它的最终状态作为自己的状态。
//其他情况,直接返回以该值为成功状态的promise对象。
Promise.resolve = (param) => {
if(param instanceof Promise) return param;
return new Promise((resolve, reject) => {
if(param && param.then && typeof param.then === 'function') {
// param 状态变为成功会调用resolve,将新 Promise 的状态变为成功,反之亦然
param.then(resolve, reject);
}else {
resolve(param);
}
})
}
reject
//Promise.reject 中传入的参数会作为一个 reason 原封不动地往下传,
Promise.reject = function (reason) {
return new Promise((resolve, reject) => {
reject(reason);
});
}
finally
Promise.prototype.finally = function(callback) {
this.then(value => {
return Promise.resolve(callback()).then(() => {
return value;
})
}, error => {
return Promise.resolve(callback()).then(() => {
throw error;
})
})
}
all
//传入参数为一个空的可迭代对象,则直接进行resolve。
//如果参数中有一个promise失败,那么Promise.all返回的promise对象失败。
//在任何情况下,Promise.all 返回的 promise 的完成状态的结果都是一个数组
Promise.all = function(promises) {
return new Promise((resolve, reject) => {
let result = [];
let len = promises.length;
if(len === 0) {
resolve(result);
return;
}
const handleData = (data, index) => {
result[index] = data;
// 最后一个 promise 执行完
if(index == len - 1) resolve(result);
}
for(let i = 0; i < len; i++) {
// 为什么不直接 promise[i].then, 因为promise[i]可能不是一个promise
Promise.resolve(promise[i]).then(data => {
handleData(data, i);
}).catch(err => {
reject(err);
})
}
})
}
race
//只要有一个 promise 执行完,直接 resolve 并停止执行
Promise.race = function(promises) {
return new Promise((resolve, reject) => {
let len = promises.length;
if(len === 0) return;
for(let i = 0; i < len; i++) {
Promise.resolve(promise[i]).then(data => {
resolve(data);
return;
}).catch(err => {
reject(err);
return;
})
}
})
}
18、实现柯理化
// 题目:实现add(1)(2)(3)
// 普通做法
//柯理化 主要有3个作用: 参数复用、提前返回和 延迟执行
function curry(fn){
//获取fn参数的数量
var n = fn.length;
//声明一个数组args
var args = [];
//返回一个匿名函数
return function(arg){
//将curryIt后面括号中的参数放入数组
args.push(arg)
//如果args中的参数个数小于fn函数的参数个数,
//则执行arguments.callee(其作用是引用当前正在执行的函数,这里是返回的当前匿名函数)。
//否则,返回fn的调用结果
if(args.length<n){
return arguments.callee // callee返回正在执行的函数本身的引用
}else{
return fn.apply('',args)
}
}
}
//或者
var curry = (fn, ...args) =>
args.length >= fn.length
? fn(...args)
: (..._args) => curry(fn, ...args, ..._args)
19、实现深拷贝
浅拷贝:只拷贝基础类型,不拷贝引用类型
var obj = { age: '12' }
var newObj = obj
1、JSON.parse
var arr1 = ['red', 'green']
JSON.parse(JSON.stringify(arr1));
缺点:
1、无法循环引用
2、无法拷贝特殊对象, RegExp, Date, Set, Map等
3、不能拷贝函数
4、会忽略undefined和symbol
2、手写一个深拷贝
/*
如果我们要拷贝的对象非常庞大时,使用 Map会对内存造成非常大的额外消耗,
而且我们需要手动清除 Map的属性才能释放这块内存,而 WeakMap会帮我们巧妙化解这个问题。
*/
function deepclone(target, map = new WeekMap()) {
if(typeof target ==='object'){
let cloneTarget =Array.isArray(target)?[]:{}
if(map.has(target)){
return target
}
map.set(target,cloneTarget)
for(const key in target){
cloneTarget[key] =clone(target[key],map)
}
return cloneTarget
}else{
return target
}
};
20、实现trim
//1、
String.prototype.trim = function () {
return this.replace(/^\s\s*/, '').replace(/\s\s*$/, '')
}
//2
String.prototype.trim = function () {
var str = this,
str = str.replace(/^\s\s*/, ''),
ws = /\s/,
i = str.length;
while (ws.test(str.charAt(--i)));
return str.slice(0, i + 1);
}
21、获取document的所有对象类型和名称
for(key in document){
// Object对象使用toString()返回[onject Object],其他则需要用call,apply才能返回证券类型
if(Object.prototype.toString(key)=='[object Object]'){
console.log(key+":"+typeof document[key])
}
}
22、大数相加
/* JS 中整数的最大安全范围可以查到是:9007199254740991
假如我们要进行 9007199254740991 + 1234567899999999999
*/
let a = '9007199254740991'
let b= "1234567899999999999"
function add(a,b){
//取两个数字的最大长度
let maxLength = Math.max(a.length,b.length)
//用0去补齐长度
a = a.padStart(maxLength,0) // padStart 从头补全
b = b.padStart(maxLength,0)
let sum = ''
let t = 0
let f =0 // 进位
for(let i=maxLength-1;i>=0;i--){
t=parseInt(a[i])+parseInt(b[i])+f
f=Math.floor(t/10)
sum = t%10+sum
}
if(f==1){
sum = '1'+sum
}
return sum
}
console.log(add(a ,b)); //结果为:1243575099254740990
23、千数分位符
/* 正则实现 */
function numFormat(num) {
var res = num.toString().replace(/\d+/, function (n) { // 先提取整数部分
return n.replace(/(\d)(?=(\d{3})+$)/g, function ($1) {
return $1 + ",";
});
})
return res;
}
24、设计模式
https://juejin.cn/post/6908528350986240014
- 工厂模式
- 单例模式
- 建造者模式
- 适配器模式
- 装饰器模式
- 代理模式
- 原型模式
- 备忘录模式
- 观察者模式
- 策略模式
1、单例模式
<body>
<button id="button">这是一个按钮</button>
<script>
function createlogin(){
let div = document.createElement('div')
div.innerHTML = '这是一个登陆界面'
div.style.display = 'none'
document.body.appendChild(div)
return div
}
function single(fn){
let result
return function(){
return result||(result = fn.apply(this,arguments))
}
}
let create = single(createlogin)
document.getElementById('button').onclick=function(){
let loginLay=create()
loginLay.style.display=('block')
}
</script>
</body>
2、发布订阅模式
var Event = (function(){
var list ={},
listen,
trigger,
remove,
// 发布
listen = function(key,fn){
if(!list[key]){
list[key]=[]
}
list[key].push(fn)
}
// 订阅
trigger = function(){
// arguments不能直接调用shift,要用call或者...
var key = Array.prototype.shift.call(arguments)
fns = list[key]
if(!fns||fns.length===0){
return false
}
for(var i=0,fn;fn=fns[i++];){
fn.apply(this,arguments)
}
}
// 取消订阅
remove = function(key,fn){
var fns = list[key]
if(!fns){
return false
}
if(!fn){
fns&&(fns.length=0)
}else{
for(var i=fns.length;i>=0;i--){
var _fn=fns[i]
if(_fn===fn){
fns.splice(i,1)
}
}
}
}
return {
listen,
trigger,
remove
}
})()
25、模块化
\* ES6 Modue 规范:JavaScript 语言标准模块化方案,浏览器和服务器通用,模块功能主要由 export 和 import 两个命令构成。export 用于定义模块的对外接口,import 用于输入其他模块提供的功能。
\* CommonJS 规范:主要用于服务端的 JavaScript 模块化方案,Node.js 采用的就是这种方案,所以各种 Node.js 环境的前端构建工具都支持该规范。CommonJS 规范规定通过 require 命令加载其他模块,通过 module.exports 或者 exports 对外暴露接口。
\* AMD 规范:全称是 Asynchronous Modules Definition,异步模块定义规范,一种更主要用于浏览器端的 JavaScript 模块化方案,该方案的代表实现者是 RequireJS,通过 define 方法定义模块,通过 require 方法加载模块
\* CMD规范:专门用于浏览器端,同样是受到Commonjs的启发,国内(阿里)诞生了一个CMD(Common Module Definition)规范。该规范借鉴了Commonjs的规范与AMD规范,在两者基础上做了改进。
与AMD相比非常类似,CMD规范(2011)具有以下特点:
define定义模块,require加载模块,exports暴露变量。
不同于AMD的依赖前置,CMD推崇依赖就近(需要的时候再加载)
推崇api功能单一,一个模块干一件事
与AMD的区别:
AMD 推崇依赖前置
CMD 推崇依赖就近
26、BOM对象
1、核心对象window,代表浏览器的一个实例,window既是通过javascript访问服务器窗口的一个接口,
window.name:框架的名称
window.top:最高层的框架
window.parent:当前框架的直接上层
window.open():导航到一个特定的URL或打开一个新的浏览器窗口,4个参数:URL,窗口目标,特性字符串(若第二个参数所指的窗口不存在,则以其特性创建一个新窗口),是否取代历史记录中当前加载的页面
window.opener:指向打开他的的原始窗口
self:指向window
2、窗口
位置:大部分浏览器:window.screenleft和window.screentop
FireFox:window.screenX和window.screenY
大小:outerWidth和outerHeight:(IE9+,Safari,Firefox:浏览器本身尺寸),(Chrome与innerWidth等相同)
innerWidth和innerHeight:视口
27、DOM对象
28、排序算法
## 不稳定的排序算法有 快选堆希
希尔排序, 选择排序,快速排序,堆排序
## 稳定的排序 插冒归基
插入排序 ,冒泡排序,归并排序,基数排序
冒泡排序
对未排序的各元素从头到尾依次比较相邻的两个元素大小关系
如果左边的队员高,则两队员交换位置
向右移动一个位置,比较下面两个队员
当走到最右端时, 最高的队员一定被放在了最右边
按照这个思路,从最左端重新开始, 这次走到倒数第二个位置的队员即可
依次类推,就可以将数据排序完成
function bubblesort3(arr) {
// 是否交换
let flash = true
// 上次没有经过交换的位置
let lastIndex = arr.length - 1
// 上次发生交换的位置
let swappedIndex = -1
while (flash) {
flash = false
for (let j = 0; j < lastIndex; j++) {
if (arr[j] > arr[j + 1]) {
arr[j + 1] = arr[j] + arr[j + 1]
arr[j] = arr[j + 1] - arr[j]
arr[j + 1] = arr[j + 1] - arr[j]
flash = true
swappedIndex = j
}
}
lastIndex = swappedIndex
}
return arr
}
选择排序
交换的次数由 O(N²) 减少到 O(N),但是比较的次数依然是 O(N²)
选定第一个索引位置,然后和后面元素依次比较
如果后面的元素,小于第一个索引位置的,则选定这个位置的元素,再与后面的依次比较
一轮比较完后,将选定的元素与第一个交换,可以确定第一个位置是最小的
然后使用同样的方法把剩下的元素逐个比较即可
可以看出选择排序,第一轮会选出最小值,第二轮会选出第二小的值,直到最后
function selectionSort1(arr){
let minIndex
let len = arr.length
for(let i=0;i<len-1;i++){
minIndex = i
for(let j=i+1;j<len;j++){
if(arr[minIndex]>arr[j]){
minIndex = j
}
}
let temp = arr[i]
arr[i]=arr[minIndex]
arr[minIndex ]=temp
}
return arr
}
插入排序
从第一个元素开始,该元素可以认为已经被排序
取出下一个元素,在已经排序的元素序列中从后向前扫描
如果该元素(已排序)大于新元素,将该元素移到下一位置
重复上一个步骤,直到找到已排序的元素小于或者等于新元素的位置
将新元素插入到该位置后,重复上面的步骤.
function insertSort2(arr){
for(let i=1;i<arr.length;i++){
let j =i
while(j>=1&&arr[j]<arr[j-1]){
let temp = arr[j]
arr[j] =arr[j-1]
arr[j-1]=temp
j--
}
}
return arr
}
希尔排序
将待排序数组按照一定的间隔分为多个子数组,每组分别进行插入排序。
这里按照间隔分组指的不是取连续的一段数组,而是每跳跃一定间隔取一个值组成一组。
逐渐缩小间隔进行下一轮排序
最后一轮时,取间隔为 1,也就相当于直接使用插入排序。但这时经过前面的“宏观调控”,
数组已经基本有序了,所以此时的插入排序只需进行少量交换便可完成。
举个例子,对数组 [84, 83, 88, 87, 61, 50, 70, 60, 80, 99] 进行希尔排序的过程如下:
第一遍(5 间隔排序):按照间隔 5 分割子数组,共分成五组,分别是 [84, 50], [83, 70], [88, 60], [87, 80], [61, 99]。
对它们进行插入排序,排序后它们分别变成: [50, 84], [70, 83], [60, 88], [80, 87], [61, 99],
此时整个数组变成 [50, 70, 60, 80, 61, 84, 83, 88, 87, 99]
第二遍(2 间隔排序):按照间隔 2 分割子数组,共分成两组,分别是 [50, 60, 61, 83, 87], [70, 80, 84, 88, 99]。
对他们进行插入排序,排序后它们分别变成:[50, 60, 61, 83, 87], [70, 80, 84, 88, 99],
此时整个数组变成 [50, 70, 60, 80, 61, 84, 83, 88, 87, 99]。这里有一个非常重要的性质:
当我们完成 2 间隔排序后,这个数组仍然是保持 5 间隔有序的。也就是说,更小间隔的排序没有把上一步的结果变坏。
第三遍(1 间隔排序,等于直接插入排序):按照间隔 1 分割子数组,分成一组,也就是整个数组。
对其进行插入排序,经过前两遍排序,数组已经基本有序了,所以这一步只需经过少量交换即可完成排序。
排序后数组变成 [50, 60, 61, 70, 80, 83, 84, 87, 88, 99],整个排序完成。
function shellSort(arr){
// 间隔序列
for(let gap=Math.floor(arr.length/2);gap>0;gap=Math.floor(gap/2)){
// 分组
for(let groupStartIndex=0;groupStartIndex<gap;groupStartIndex++){
//
for(let currentIndex = groupStartIndex+gap;currentIndex<arr.length;currentIndex+=gap){
let currentNumber = arr[currentIndex]
let preIndex = currentIndex-gap
while(preIndex>=groupStartIndex&¤tNumber<arr[preIndex]){
// 向后挪
arr[preIndex+gap] = arr[preIndex]
preIndex-=gap
}
arr[preIndex+gap]=currentNumber
}
}
}
return arr
}
快速排序
- 快速排序的平均效率是 O(N * logN)
快速排序最重要的思想是分而治之
比如我们有这样一堆数字需要排序(13,81,92,43,65,31,57,26,75,0)
从其中选出了 65(其实可以是选出任意的数字)
通过算法:将所有小于 65 的数字放在 65 的左边,将所有大于 65 的数字放在 65 的右边
递归的处理左边的数据(比如你选择 31 来处理左侧),递归的处理右边的数据(比如选择 75 来处理右侧,当然选择 81 可能更合适)
排序完成
function quickSort(arr){
quickSort1(arr,0,arr.length-1)
return arr
}
function quickSort1(arr,start,end){
// 如果区域内数字少于2个
if(start>=end) return
// 将数字分区,并获得中间值的下标
let middle = partition(arr,start,end)
// 对左边区域快速排序
quickSort1(arr,start,middle-1)
// 对右边快速排序
quickSort1(arr,middle+1,end)
}
// 分区函数
function partition(arr,start,end){
// 选第一个数为基数
let pivot=arr[start]
// 左边界
let left = start +1
// 右边界
let right = end
while(left<right){
// 找到第一个大于基数的位置
while(left<right&&arr[left]<=pivot) left++
// 找到第一个小于基数的位置
while (left<right&&arr[right]>=pivot) right--
// 交换这两个数,使得左边分区都小于或等于基数,右边分区大于或等于基数
if(left<right){
exchange(arr,left,right)
left++
right--
}
}
// 如果left和right相等,单独比较arr[right]和privot
if(left==right&&arr[right]>pivot) right--
// 基数和中间数交换
exchange(arr,start,right)
// 返回中间值的下标
return right
}
function exchange(arr,i,j){
let temp = arr[i]
arr[i]=arr[j]
arr[j]=temp
}
堆排序
堆排序时间复杂度:构建时为O(n) 平均为 O(nlogn) 空间复杂度为O(1)
完全二叉树有个特性:从任何一个节点 i 出发,都可以通过计算得到这个节点的父节点与两个子节点
父节点 = Math.floor( (i - 1) / 2 )
左子节点 = 2 * i + 1
右子节点 = 2 * (i + 1)
这里讨论大顶堆:
将待排序的数组构造成一个大顶堆。此时整个数组的最大值就是堆顶的根节点。
将它移走,其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值。
然后将剩余的 n-1 个元素又重新构造成堆,这样就又能得到次大值。
如此反复操作,直到只剩余一个元素,就能得到一个有序数组了。
function heapSort(arr){
buildMaxHeap(arr)
for(let i=arr.length-1;i>0;i--){
// 将最大值放到数组最后
exchange(arr,0,i)
// 调整剩余数组,使其满足大顶堆
maxHeapify(arr,0,i)
}
return arr
// 初始化大顶堆
function buildMaxHeap(arr){
for(let i=Math.floor(arr.length/2)-1;i>=0;i--){
maxHeapify(arr,i,arr.length)
}
}
// 调整大顶堆,第三个参数表示剩余未排序的数字的数量,也是剩余堆的大小
function maxHeapify(arr,i,heapSize){
// 左子结点下标
let l = 2*i+1
//右结点下标
let r = l+1
// 记录根结点、左子树结点、右子树结点三者中的最大值下标
let largest = i
// 与左子树结点比较
if(l<heapSize&&arr[l]>arr[largest]){
largest = l
}
// 与右子树结点比较
if(r<heapSize&&arr[r]>arr[largest]){
largest = r
}
if(largest!=i){
// 将最大值交换为根结点
exchange(arr,i,largest)
// 再次调整交换数字后的大顶堆
maxHeapify(arr,largest,heapSize)
}
}
// 交换元素
function exchange(arr,i,j){
let temp = arr[i]
arr[i]=arr[j]
arr[j]=temp
}
}
归并排序
时间复杂度为:nlog(n),空间复杂度为n
归并排序采用的是分治的思想,首先是“分”
将一个数组反复二分为两个小数组,直到每个数组只有一个元素
其次是“治”,从最小数组开始,两两按大小顺序合并,直到并为原始数组大小
function mergeSort2(arr){
if(arr.length==0) return
let result = new Array(arr.length)
mergeSort3(arr,0,arr.length-1,result)
return arr
}
// 对arr的[start,end]区间进行归并排序
function mergeSort3(arr,start,end,result){
// 只剩下一个数字,停止拆分
if(start==end) return
let middle = Math.floor((start+end)/2)
// 拆分左边区域
mergeSort3(arr,start,middle,result)
// 拆分右边区域
mergeSort3(arr,middle+1,end,result)
// 合并两边区间
merge1(arr,start,end,result)
}
function merge1(arr,start,end,result){
let end1 = Math.floor((start+end)/2)
let start2= end1+1
//用来遍历的指针
let index1 = start
let index2 = start2
while(index1<=end1&&index2<=end){
result[index1+index2-start2]=arr[index1]<=arr[index2]?arr[index1++]:arr[index2++]
}
// 将剩余数字补到结果数组之后
while(index1<=end1){
result[index1+index2-start2] = arr[index1++]
}
while(index2<=end){
result[index1+index2-start2] = arr[index2++]
}
// 将 result 操作区间的数字拷贝到 arr 数组中,以便下次比较
while(start<=end){
arr[start] = result[start++]
}
}
将最大值交换为根结点
exchange(arr,i,largest)
// 再次调整交换数字后的大顶堆
maxHeapify(arr,largest,heapSize)
}
}
// 交换元素
function exchange(arr,i,j){
let temp = arr[i]
arr[i]=arr[j]
arr[j]=temp
}
}
#### 归并排序
时间复杂度为:nlog(n),空间复杂度为n
归并排序采用的是分治的思想,首先是“分”
将一个数组反复二分为两个小数组,直到每个数组只有一个元素
其次是“治”,从最小数组开始,两两按大小顺序合并,直到并为原始数组大小
```javascript
function mergeSort2(arr){
if(arr.length==0) return
let result = new Array(arr.length)
mergeSort3(arr,0,arr.length-1,result)
return arr
}
// 对arr的[start,end]区间进行归并排序
function mergeSort3(arr,start,end,result){
// 只剩下一个数字,停止拆分
if(start==end) return
let middle = Math.floor((start+end)/2)
// 拆分左边区域
mergeSort3(arr,start,middle,result)
// 拆分右边区域
mergeSort3(arr,middle+1,end,result)
// 合并两边区间
merge1(arr,start,end,result)
}
function merge1(arr,start,end,result){
let end1 = Math.floor((start+end)/2)
let start2= end1+1
//用来遍历的指针
let index1 = start
let index2 = start2
while(index1<=end1&&index2<=end){
result[index1+index2-start2]=arr[index1]<=arr[index2]?arr[index1++]:arr[index2++]
}
// 将剩余数字补到结果数组之后
while(index1<=end1){
result[index1+index2-start2] = arr[index1++]
}
while(index2<=end){
result[index1+index2-start2] = arr[index2++]
}
// 将 result 操作区间的数字拷贝到 arr 数组中,以便下次比较
while(start<=end){
arr[start] = result[start++]
}
}