javascript常见面试题

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。

转换流程:

  1. 如果Symbol.toPrimitive()方法,优先调用再返回
  2. 调用valueOf(),如果转换为原始类型,则返回
  3. 调用toString(),如果转换为原始类型,则返回
  4. 如果都没有返回原始类型,会报错

== 不像 === 那样严格,对于一般情况,只要值相等,就返回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

img

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 状态的回调函数

优点:

链式调用解决回调地狱

  1. 多层嵌套的问题。
  2. 每种任务的处理结果存在两种可能性(成功或失败),那么需要在每种任务执行结束后分别处理这两种可能性。
  • 回调函数延迟绑定
  • 返回值穿透
  • 错误冒泡

缺点:对于长的链式操作来说,看起来是一堆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&&currentNumber<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++]
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值