JS高频面试题

文章目录

数据类型

什么是基本数据类型?什么是引用数据类型?以及各个数据类型是如何存储的?(5星)

JS内存和其他语言的差别
在其他常见的语言中,用来保存变量和对象的内存一般分为栈内存和堆内存,而在JavaScript中,所谓的栈和堆都是放在堆内存中的,而在堆内存中,JS把其分为栈结构和堆结构,这里常被误认为是堆内存和栈内存,但是我们可以把它简称为栈和堆。
在这里插入图片描述
栈和堆的特点和区别
栈(stack) :栈会自动分配内存空间,会自动释放,存放基本类型引用数据类型的变量
所有在函数/方法中定义的变量都是放在栈内存中,随着函数/方法的执行结束,这个函数/方法的内存栈也自然销毁。
优点:存取速度比堆快。 缺点:存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
堆(heap) :动态分配的内存,大小不定也不会自动释放,存放引用类型的值,指那些可能由多个值构成的对象,保存在堆内存中。
堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能被另一个引用变量所引用(参数传递)。创建对象是为了反复利用。

基本数据类型 数据直接存储在栈中,按值进行存储和访问的,每个变量都有自己的内存空间和值。当你将一个基本数据类型的值赋给另一个变量时,会创建一个新的值并将其复制到新变量中。因此,基本数据类型的变量之间是相互独立的,修改其中一个变量的值不会影响其他变量。基本数据类型包括:

  • 布尔值(Boolean):表示真或假。
  • 数字(Number):表示数字,包括整数和浮点数。
  • 字符串(String):表示文本。
  • undefined:表示未定义的值。
  • null:表示空值。
  • Symbol(ES6新增):表示一种唯一的、不可变的值。

基本数据类型被保存在栈结构中,在栈结构中会有全局执行环境(也称全局执行上下文),在全局作用域中的基本数据类型就是被保存全局执行环境中的。

var a = 10;
var b = a;
b = 20;

在这里插入图片描述

首先var 定义的变量a 加入到全局执行环境中,并且赋值为10,然后定义变量b并且把a的值赋值给变量b,此时b的值为10,最后b的值被修改为20,最终的执行结果为a输出为10,b输出为20
注:实际上是有变量提升,先定义后赋值,不过理解存储原理变量提升可以暂且忽略

引用数据类型栈中仅保存数据的引用地址,数据存储在堆中,通过引用地址访问数据。栈就是一本书的目录,堆是目录对应的内容。复杂数据类型包括:

  • 对象(Object):表示键值对的集合。
  • 数组(Array):表示有序的值的列表。
  • 函数(Function):表示可执行的代码块。

当你将一个复杂数据类型的值赋给另一个变量时,实际上是将引用地址复制给了新变量。这意味着两个变量指向同一个对象(或数组、函数)在内存中的位置。因此,改变其中一个变量所指向对象的属性或元素,另一个变量也会反映出这些改变,因为它们引用的是同一个对象。

引用类型是存放在堆内存中的对象,变量其实是保存的在栈内存中的一个指针(保存的是堆内存中的引用地址),这个指针指向堆内存。
引用类型数据在栈内存中保存的实际上是对象在堆内存中的引用地址。通过这个引用地址可以快速查找到保存中堆内存中的对象。
对比上一段代码,如果我们把10和20换成一个数组又会发生什么呢?

var arr = [1,2,3];
var arr1 = arr;
arr1[1] = 22;

在这里插入图片描述

首先定义了arr变量,数组也是一个对象,所以在栈结构的全局执行环境中我们保存的实际上是一个地址值(指针),而真正的数组是被保存在了堆结构中。保存对象首先会在堆结构中开辟一块内存空间,然后把地址值赋值给变量,栈结构中只保存对象的地址值。这里我们可以看到arr的保存的地址值赋值给了arr1,所以此时arr和arr1都指向了同一个堆结构中的对象,此时我们通过数组的索引arr[1]去修改第二个元素为22,无论我们通过arr还是arr1访问这个数组,实际上都是在访问相同的对象。所以我们两次打印输出的结果都是[1,22,3]。

数据类型 USONB 你很牛逼 U:undefined S:string symbol O:object N:number null B:boolean

JS有哪些内置对象

基本对象Object,内置对象有Array Math Number String Date JSON

类型转换

为什么typeof null是Object?(4星)

  • 因为在JavaScript中,如果二进制前三位都是0的话,系统会判断为是Object类型,而null的二进制全是0,自然也就判断为Object

判断数据类型有几种方法 ?(5星)

  • typeof

    • 缺点:typeof null的值为Object,无法分辨是null还是Object
  • instanceof 判断一个实例是否属于某种类型
    Object instanceof Object //true

    • 缺点:只能判断对象是否存在于目标对象的原型链上,不能判断字面量的基本数据类型
  • Object.prototype.toString.call()

    • 一种最好的基本类型检测方式 Object.prototype.toString.call() ;它可以区分 null 、 string 、boolean 、 number 、 undefined 、 array 、 function 、 object 、 date 、 math 数据类型。

    • 缺点:不能细分为谁谁的实例

      // -----------------------------------------typeof
        typeof undefined // 'undefined' 
        typeof '10' // 'String' 
        typeof 10 // 'Number' 
        typeof false // 'Boolean' 
        typeof Symbol() // 'Symbol' 
        typeof Function // ‘function' 
        `typeof null // ‘Object’ `
        typeof [] // 'Object' 
        typeof {} // 'Object'
        // -----------------------------------------instanceof
        function Foo() { }
        var f1 = new Foo();
        var d = new Number(1)
        console.log(f1 instanceof Foo);// true
        console.log(d instanceof Number); //true
        console.log(123 instanceof Number); //false   -->不能判断字面量的基本数据类型
        // -----------------------------------------constructor
        var d = new Number(1)
        var e = 1
        function fn() {
          console.log("ming");
        }
        var date = new Date();
        var arr = [1, 2, 3];
        var reg = /[hbc]at/gi;
        console.log(e.constructor);//ƒ Number() { [native code] }
        console.log(e.constructor.name);//Number
        console.log(fn.constructor.name) // Function 
        console.log(date.constructor.name)// Date 
        console.log(arr.constructor.name) // Array 
        console.log(reg.constructor.name) // RegExp
        //-----------------------------------------Object.prototype.toString.call()
        console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]" 
        console.log(Object.prototype.toString.call(null)); // "[object Null]" 
        console.log(Object.prototype.toString.call(123)); // "[object Number]" 
        console.log(Object.prototype.toString.call("abc")); // "[object String]" 
        console.log(Object.prototype.toString.call(true)); // "[object Boolean]" 
        function fn() {
          console.log("ming");
        }
        var date = new Date();
        var arr = [1, 2, 3];
        var reg = /[hbc]at/gi;
        console.log(Object.prototype.toString.call(fn));// "[object Function]" 
        console.log(Object.prototype.toString.call(date));// "[object Date]" 
        console.log(Object.prototype.toString.call(arr)); // "[object Array]"
        console.log(Object.prototype.toString.call(reg));// "[object RegExp]"
    

instanceof原理(5星)

instanceof原理:判断对象是否存在于目标对象的原型链上,一层一层向上查找,一直找到null为止 扩展到原型链上
a instanceof b a为对象 b为目标对象
a是否是b的实例对象

   function Foo() { }
    var f1 = new Foo();
    console.log(f1 instanceof Foo);// true
   function myInstance(L, R) {//L代表instanceof左边,R代表右边
      var RP = R.prototype
      var LP = L.__proto__
      while (true) {
        if(LP == null) {
          return false
        }
        if(LP == RP) {
          return true
        }
        LP = LP.__proto__ 
      }
    }
    console.log(myInstance({},Object)); 

null和undefined的区别(4星)

null:

  1. 此处不应该有值
  2. typeof null = object
  3. 转为数值为0
  4. 作为函数的参数,表示该函数的参数不是对象;作为对象原型链的终点

undefined:

  1. 此处应该有一个值但是还没有定义
  2. typeof undefined = undefined
  3. 转为数值为NaN
  4. 例如变量被声明了但没有赋值,就等于undefined;函数没有返回值默认返回undefined;对象没有赋值的属性,该属性的值为undefined

=====有什么区别?(5星)

最大的区别在于,全等号不执行类型转换。
相等是经过类型转换数值相等 返回true
全等号由三个等号表示(===),只有在无需类型转换运算数就相等的情况下,才返回 true

非全等号由感叹号加两个等号(!==)表示,只有在无需类型转换运算数不相等的情况下,才返回 true。

==是非严格意义上的相等,

  • 先检查数据类型是否相同

  • 如果相同,则比较两个数是否相等

  • 如数据类型类型不同,则先转换成相同的数据类型,再进一步进行比较。

    • Null == Undefined ->true
    • String == Number ->先将String转为Number,在比较大小
    • Boolean == Number ->先将Boolean转为Number,在进行比较
    • Object == String,Number,Symbol -> Object 转化为原始类型
      ===是严格意义上的相等,会比较两边的数据类型和值大小
  • 先检查的数据类型是否相同

  • 若相同,则比较二者是否相等,若相等返回true

  • 若不同,直接返回false

1=='1'//ture
1==[1]//true
1==false//false
1==true//true

1==='1'//false
1===[1]//false

//特例
null==undefined //true
null===undefined //false
NaN==NaN //false
NaN===NaN //false
//NaN(not a number)是一个特殊的数值,这个数值用于表示一个本来要返回数值的操作数未返回数值的情况,避免报错。NaN与任何值都不相等,包括NaN自身。

判断是否为整数(4星)

1、ES6 Number.isInteger()

Number.isInteger(3) //true
Number.isInteger(3.2) //false

2、Math.ceil()、Math.floor()、Math.round()方法

Math ceil()对一个数进行上取整 ceil天花板
Math.floor()对一个数进行下取整 floor地板
Math.round()四舍五入取整

3、使用取余的方式,任意整数取1的余数都是0

js 具有类型自动转换的效果,先判断数据类型是否是 number,然后再判断取 1 的余数是否等于 0.

function isInteger(num) {
  return typeof num === 'number' && num % 1 === 0
}

3、Math.floor

手写call、apply、bind (5星)

call、apply、bind都是改变this指向的方法
call

fun. call (thisArg, argl, arg2, ..) 
thisArg:在fun函数运行时指定的this值arg1,arg2:传递的其他参数

call可以直接调用函数,可以改变函数内的this指向  
call的主要作用可以实现继承
 var o = {
    name: 'andy'
  }
  function fn(a, b) {
    console.log(this)//原本this指向window 当执行fn.call后指向o
    console.log(a + b)
  }
  fn.call(o, 1, 2)

bind
bind:语法和call一模一样,区别在bind不调用函数

fn.call(obj, 1, 2); // 改变fn中的this,并且把fn立即执行
fn.bind(obj, 1, 2); // 改变fn中的this,fn并不执行 等待事件触发

apply
和call基本上一致,唯一区别在于传参方式
apply把放到一个数组(或者类数组)中传递进去

fn.call(obj, 1, 2);
fn.apply(obj, [1, 2]);

因为我们需要让所有的地方都可以去访问到,所以我们把自定义的函数写在Function的原型上,因为所有的函数 instanceof Function都是会返回true的,所以定义在Function的prototype上call和bind我们接收第二个以后的参数,所以可能会用到slice方法,而apply直接接受第二个参数就行了。那么如何显式的绑定this呢,如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。拿取参数的话,我们还是用最简单的方式arguments,没有参数的情况下我们就直接将函数无参返回,如果没有传入对象的话直接指向window,这样的话我们的手写函数就和原来的call,apply和bind没什么两样了。

Function.prototype.MyCall = function(context){
//直接将函数放在Function的prototype上,方便调用
		const ctx = context || window;
//这里是将传入对象存到ctx里边,如果没有传入的话就指向window,因为原来的call也是这样的
		ctx.func = this;
//改变this的指向,换句话说相当于是把这个函数直接给到传入的对象
		const args = Array.from(arguments).slice(1);
//这里就是要去拿到函数的参数,把类数组转为数组之后,减去第一个,给到args备用
		const res = arguments.length > 1 ? ctx.func(...args) : ctx.func();
//就是说如果有参数,把args解构传进去,没有的话就不传了
		delete ctx.func;
//这个时候的func已经没有用了,直接返回res
		return res
//一步一注释应该是简单易懂的
	}

apply跟call是一样的,只是参数的处理简单一点,就不一步一注释了。

Function.prototype.MyApply = function(context){
	const ctx = context || window;
	ctx.func = this;
	const res = arguments[1] ? ctx.func(...arguments[1]):ctx.func();
	delete ctx.func;
	return res
}

bind有所不同,bind函数返回一个绑定函数,最终调用需要传入函数实参和绑定函数的实参。如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。

Function.prototype.MyBind = function(context){
	console cxt = JSOn.parse(JSON.stringify(context)) || window;
	cxt.func = this;
	const args = Array.from(arguments).slice(1);
	return function(){
		const allArgs = args.concat(Array.from(arguments));
		return allArgs.length > 0 ? cxt.func(...allArgs):cxt.func();
	}

字面量创建对象和new创建对象有什么区别?手写一个new ?new时内部发生了什么?(5星)

区别:

字面量:

var obj = {
    name: "小明",
    sayHi:function () {
        console.log("我是:" + this.name);
    },
   }
  • 字面量创建对象更简单,方便阅读
  • 不需要作用域解析,速度更快

new:创建对象的过程复杂,过程是这样的

  • 创建一个新对象
  • 使新对象的__proto__指向原函数的原型对象
  • 改变this指向,指向新的对象并执行该函数,执行结果保存起来作为result
  • 判断执行函数的结果是不是null或Undefined,如果是则返回之前的新对象,如果不是则返回之前的执行结果result

手写new

    // 手写一个new
       var obj = new fn()
    function myNew(fn, arg) {
      // 创建一个空对象
      let obj = {}
      // 使空对象的隐式原型指向原函数的显式原型
      obj.__proto__ = fn.prototype
      // this指向obj
      let result = fn.call(obj, arg)
      // 返回
      return result instanceof Object ? result : obj
    }
        console.log(null instanceof Object)//false
        console.log(undefined instanceof Object)//false

字面量和new出来的对象和 Object.create(null)创建出来的对象有什么区别 ?(3星)

  • 字面量和new创建出来的对象会继承Object的方法和属性,他们的隐式原型会指向Object的显式原型,
  • Object.create(null)创建出来的对象原型为null,作为原型链的顶端,自然也没有继承Object的方法和属性

原型链和原型

什么是原型?什么是原型链?如何理解?(5星)

原型: 原型分为隐式原型(__proto__属性)和显式原型(原型对象prototype),每个对象都有一个隐式原型,它指向自己的构造函数的显式原型。

原型链: 多个__proto__组成的集合成为原型链

实例对象的__proto__指向构造函数原型对象

构造函数原型对象的__proto__指向Object原型对象

Object的原型对象__proto__指向null

每一个构造函数都有原型对象prototype,把所有不变的的方法都直接定义在原型对象上,然后从构造函数中new出来的实例对象就可以共享这些方法
实例对象都会有__proto__属性,指向构造函数的原型对象prototype,之所以实例对象可以使用构造函数原型对象的属性和方法,就是因为对象有__proto__属性的存在。

构造函数.prototype === 实例对象.__proto__

原型对象中有一个constructor属性,指向原来的构造函数

示意图:

在这里插入图片描述

JavaScript的成员查找机制(3星)

1.当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。

2.如果没有就查找它的原型(也就是__proto__指向的构造函数的原型对象)

3.如果还没有就查找原型对象的原型(即Object的原型对象)

4.依此类推一直找到Object为止(Object的原型对象__proto__=>查找机制的终点为null).

5.__proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。

执行栈和执行上下文

什么是作用域,什么是作用域链? (4星)

  • 规定变量和函数的可使用范围称为作用域
  • 查找变量或者函数时,需要从局部作用域到全局作用域依次查找,这些作用域的集合称作用域链。作用域链就是代码块内有代码块,跟常规编程语言一样,上级代码块中的局部变量下级可用
  • 作用域链的作用是保证执行环境里有权访问的变量和函数是有序的,作用域链的变量只能向上访问,变量访问到window对象即被终止

什么是执行上下文,什么是执行栈?(4星)

执行上下文(也就是代码的执行环境)

  • 全局执行上下文

    创建一个全局的window对象,并规定this指向window,执行js的时候就压入栈底,关闭浏览器的时候才弹出

  • 函数执行上下文

    每次函数调用时,都会新创建一个函数执行上下文

  • eval执行上下文

    eval的功能是把对应的字符串解析成JS代码并运行

在这里插入图片描述
执行栈:具有先进后出结构,用于存储代码执行期间创建的所有执行上下文

  • 当进入一个执行环境,就会创建出它的执行上下文,然后进行压栈,当程序执行完成时,它的执行上下文就会被销毁,进行弹栈
  • 栈底永远是全局环境的执行上下文,栈顶永远是正在执行函数的执行上下文
  • 只有浏览器关闭的时候全局执行上下文才会弹出

闭包

什么是闭包?闭包的作用?闭包的应用? (5星)

闭包(Closure)是指在函数内部创建的函数,它可以访问并持有该函数所在的环境中的变量,即使在外部函数执行结束后,闭包仍然可以访问这些变量。
内层作用域下可以获取外层作用域下的变量,而外层作用域下无法获取内层作用域下的变量。而闭包的出现,就解决了这个问题

(函数内部可以直接读取全局变量,但是在函数外部无法读取函数内部的局部变量。这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!)

特性:
函数内再嵌套函数
内部函数可以引用外层的参数和变量
参数和变量不会被垃圾回收机制回收
作用
能够实现封装和缓存,避免全局变量的污染

缺点:
消耗内存、不正当使用会造成内存泄漏(有一块内存空间被长期占用,而不被释放)
解决方法是,在退出函数之前,将不使用的局部变量全部删除
应用

  • 防抖和节流

  • 封装私有变量

  • for循环中的保留i的操作

  • JS设计模式中的单例模式

    单例模式:用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象

  • 函数柯里化

基本步骤分析:

  1. 定义外部函数,要保护的变量以及可外部访问的内部函数。通过内部函数操作受保护变量,并将内部函数返回。
  2. 调用外部函数获取内部函数,然后基于内部函数操作受保护变量。
    function outer() {//外部函数
      var count = 0; //受保护变量(这个变量外部不可直接使用)
      return function () {//内部函数
        count++; //通过内部函数操作受保护变量
        console.log(count);
      };
    }//上面整个函数就是闭包设计
    var inner = outer(); //调用外部函数获取内部函数
    //匿名函数作为outer的返回值被赋值给了inner相当于inner=function () { count++;  console.log(count);} 
    //匿名函数被赋予全局变量,并且匿名函数内部引用着outer函数内的变量count,所以变量count无法被销毁

    inner(); //调用内部函数操作受保护变量

1.闭包作为返回值

在这里插入图片描述
在这段代码中,a()中的返回值是一个匿名函数,这个函数在a()作用域内部,所以它可以获取a()作用域下变量name的值,将这个值作为返回值赋给全局作用域下的变量b,实现了在全局变量下获取到局部变量中的变量的值

在这里插入图片描述
一般情况下,在函数fn执行完后,就应该连同它里面的变量一同被销毁,但是在这个例子中,匿名函数作为fn的返回值被赋值给了fn1,这时候相当于fn1=function(){var n = 0 … },并且匿名函数内部引用着fn里的变量num,所以变量num无法被销毁,而变量n是每次被调用时新创建的,所以每次fn1执行完后它就把属于自己的变量连同自己一起销毁,于是乎最后就剩下孤零零的num,于是这里就产生了内存消耗的问题
return function 只要函数遇见return就把后面的结果返回给函数的调用者

2.闭包作为参数
在这里插入图片描述
在这段代码中,函数fn1作为参数传入立即执行函数中,在执行到fn2(30)的时候,30作为参数传入fn1中,这时候if(x>num)中的num取的并不是立即执行函数中的num,而是取创建函数的作用域中的num这里函数创建的作用域是全局作用域下,所以num取的是全局作用域中的值15,即30>15,打印30

     var a = 1,
        b = 0;
      function A(a) {
        A = function (b) {
          alert(a + b++);
        };
        alert(a++);
      }
      A(1);//'1'
      A(2);//'4'

函数A中的A没有用var声明,所以属于全局变量,重写赋值函数A,又形成了闭包

闭包机制:闭包创建后,可以保存创建时的活动对象。
自加操作符:i++先执行再相加,++i先加在执行。如var a = 0; var b = 1; var c = a+b++,console.log(c,b) // 1 2

第一次调用函数A时,函数不调用自己不执行,所以 A = function (b) {alert(a + b++);}没有执行,执行到alert(a++)时,a++先执行后自加,所以a++为1,a为2,故A(1)=‘1’

第二次调用函数A时,A覆盖了函数A;

由于标识符A是在全局作用域定义的,所以在函数内部被重新赋值,在全局作用域也可以访问到重新赋值后的函数。 此时,也创建了一个闭包,该闭包保存了创建环境的活动对象。

执行到A(2),即b=2,alert(a + b++)=‘4’,A(2)输出’4’

继承

说一说 JS 中的常用的继承方式有哪些?以及各个继承方式的优缺点 (5星)

通过继承 ,子类不仅可以具有父类的方法和属性还可以加入新的属性和方法或者修改父类的属性和方法。
答:
原型链继承,就是让对象实例通过原型链的方式串联起来,当访问目标对象的某一属性时,能顺着原型链进行查找,从而达到类似继承的效果。
子的原型对象为new一个父的实例 Son.prototype = new Father(‘zs’);

function Father(name) {
  this.name = name;
}
Father.prototype.money = function () {
  console.log(1000);
};
function Son() {}
Son.prototype = new Father('zs');
var son = new Son('zs');
console.log(son.name);
son.money();

缺点:当某一个实例修改原型链上某一个属性时,如果实例类型是引用类型,那么其它实例的属性也会被修改。

借用构造函数实现继承父类型属性
原理:通过call()把父类型的this指向子类型的this,这样就可以实现子类型继承父类型的属性

 function Father(name) {
    this.name = name;
 }

 function Son(name) {
    Father.call(this,name);//此时父类的this指向子类的this 并调用函数
 }

var son= new Son('zs');
 console.log(son.name);//zs

缺点:不能继承父级原型链上的属性

组合继承

使用原型链继承共享的方法,通过借用构造函数继承实例属性
原理: 子类构造函数中使用Parent.call(this);的方式可以继承写在父类构造函数中this上绑定的各属性和方法;

  
function Child(name,age){
    // 继承属性
    Parent.call(this, name)
    this.age=age
}
// 继承方法
Child.prototype = new Parent()
Child.prototype.constructor = Child;

es6 extend继承

es6 class 实现继承使用关键字 extend 表明继承自哪个父类,并且在子类构造函数中必须调用 super 关键字,super关键字用于访问和调用对象父类上的函数。

    //  子类只要继承父类,可以不写constructor,一旦写了,则在constructor 中的第一句话必须是 super
    class Son3 extend Father {
      constructor(y) {
        super(200)  // super(200) => Father.call(this,200)
        this.y = y
      }
    }

内存泄露、垃圾回收机制

什么是内存泄漏? (5星)

答:

内存泄漏是指不再用的内存没有被及时释放出来,导致该段内存无法被使用

为什么会导致的内存泄漏? (5星)

答:
某段内存我们无法在通过js访问某个对象,而垃圾回收机制却认为该对象还在被引用,因此垃圾回收机制不会释放该对象,导致该块内存永远无法释放,积少成多,系统会越来越卡以至于崩溃

哪些操作会造成内存泄漏?(4星)

  • 未使用 var 声明的全局变量

  • 作用域未释放(闭包)

  • 定时器未清除

  • 事件监听为空白

    如何优化?

    1. 全局变量先声明在使用
    2. 避免过多使用闭包。
    3. 注意清除定时器和事件监听器。

垃圾回收机制都有哪些策略? (5星)

垃圾回收机制:当程序创建对象,数组等引用类型实体时,系统会在堆内存中为之分配一块内存区,对象就保存在内存区中,当内存不再被任何引用变量引用时,这块内存就变成了垃圾,等待垃圾回收机制去进行回收。

答:

  • 标记清除法

    • 垃圾回收器会在运行的时候给内存中的所有变量加上标记,然后去掉执行环境中的变量以及被执行环境中变量所引用的变量(闭包),在这些完成之后仍存在标记的就是要删除的变量了
  • 引用计数法

    ​ 原理:跟踪记录每个值被引用的次数,当计数变为0的时候,说明无法访问该值了,垃圾回收机制清除该对象。

    • 当声明一个变量并给该变量赋值一个引用类型的值时候,该值的计数+1,当该值赋值给另一个变量的时候,该计数+1,当该值被其他值取代的时候,该计数-1,当计数变为0的时候,说明无法访问该值了,垃圾回收机制清除该对象

深拷贝和浅拷贝(5星)

浅拷贝

浅拷贝因为对象只会被拷贝最外部的一层,至于更深层的对象,则依然是通过引用指向同一块堆内存,这就造成了如果对新对象进行修改,也会影响原来的对象
如果浅拷贝的数据 修改 b中的数据 那么原来的数据的b仍然发生修改

let obj = {
    a: 100,//一层
    b:[10,20,30],//两层
    c:{
        x:10
    },//两层
    d: /^\d+$/
}

方法1:使用对象的解构

对象的扩展运算符(…)用于取出对象的所有可遍历属性,拷贝到当前对象中。

let obj2 = {...obj}

方法2:使用循环

对象循环我们使用 for in 循环,但for in 循环会遍历到对象的继承属性,我们只需要它的私有属性,所以可以加一个判断方法:hasOwnProperty 保留对象私有属性hasOwnProperty() 方法只检测检测当前对象本身,只有当前对象本身存在该属性时才返回 true,

let obj2 = {}
for (let i in obj){
    if(!obj.hasOwnProperty(i)) break;  // 这里使用continue也可以
    obj2[i] = obj[i]
}

深拷贝

方法1:_.cloneDeep

lodash 提供了_.cloneDeep方法用于深拷贝

let deep = _.cloneDeep(obj)
console.log(deep === obj) //false

方法2:JSON.stringify,JSON.parse

//其原理是利用 JSON 对象的 stringify 方法将对象转换为字符串,然后再使用 parse 方法将字符串转换回对象。这个过程中,所有的属性和值都会被完全复制,从而实现深拷贝
let obj2 = JSON.parse(JSON.stringify(obj))

存在的问题: 遇到正则会变为空对象,函数为空,日期会变为字符串

方法3:万能方法

首先过滤特殊情况 =null 类型不属于Object 还有去掉正则 然后for in循环 利用hasOwnProperty获取对象的继承属性

function deepClone(obj){
    //过滤特殊情况
    if(obj === null) return null;
    if(typeOf obj !== "Object") return obj;
    if(obj instanceof RegExp){
        return new RegExp(obj)
    }
    //不直接创建空对象的目的是拷贝的结果和之前保持相同的所属类
    let newObj = new Obj.constructor 
    //也可以这么写:
    //let newObj = obj instanceof Array?[]:{}
    for (let i in obj){
        if(obj.hasOwnProperty(i)){
            newObj[i] = deepClone(obj[i])
        }
    }
    return newObj;
}
let obj2 = deepClone(obj)
console.log(obj.c === obj2.c) //fasle 表示指向的不同一内存

浅拷贝是指创建一个新对象或数据结构,新对象的属性或元素与原始对象相同,但是对于对象中的引用类型属性或元素,只是复制了引用,而不是创建新的引用。因此,原始对象和浅拷贝对象会共享同一个引用类型的属性或元素。

深拷贝是指创建一个全新的对象或数据结构,并且递归地复制原始对象的所有属性或元素,包括引用类型的属性或元素。这意味着原始对象和深拷贝对象拥有完全独立的副本,彼此之间没有任何关联。

下面是一个示例来说明深拷贝和浅拷贝的区别:

// 原始对象
const originalObj = {
  name: "John",
  age: 25,
  hobbies: ["reading", "running"]
};

// 浅拷贝
const shallowCopyObj = Object.assign({}, originalObj);

// 修改浅拷贝对象的属性
shallowCopyObj.name = "Jane";
shallowCopyObj.hobbies.push("swimming");

console.log(originalObj); // { name: "John", age: 25, hobbies: ["reading", "running", "swimming"] }  //原来的数据也发生了修改
console.log(shallowCopyObj); // { name: "Jane", age: 25, hobbies: ["reading", "running", "swimming"] }

// 深拷贝
const deepCopyObj = JSON.parse(JSON.stringify(originalObj));

// 修改深拷贝对象的属性
deepCopyObj.name = "Alice";
deepCopyObj.hobbies.push("painting");

console.log(originalObj); // { name: "John", age: 25, hobbies: ["reading", "running", "swimming"] }
console.log(deepCopyObj); // { name: "Alice", age: 25, hobbies: ["reading", "running", "painting"] }

在上述示例中,浅拷贝对象的修改会影响原始对象,因为它们共享相同的引用类型属性。而深拷贝对象的修改不会影响原始对象,因为它们拥有独立的副本。

需要注意的是,深拷贝可能会导致性能上的损耗,尤其是在处理大型对象或嵌套层级较深的对象时。此外,对于包含循环引用的对象,使用简单的深拷贝方法可能会导致无限递归,因此需要额外的处理。

事件循环Event Loop

为什么JS是单线程的?(5星)

js最初设计是运行在浏览器中的,里面有可视的Dom,为了防止多个线程同时操作DOM,带来渲染冲突问题,所以js执行器被设计成单线程

异步执行机制(5星)

JS的异步是通过回调函数实现的
一般而言,异步任务有以下三种类型
1、普通事件,如click、resize等
2、资源加载,如load、error等
3、定时器,包括setinterval、setTimeout等
异步任务相关回调函数添加到任务队列中(任务队列也称为消息队列)。

事件循环:

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个任务队列(task queue)。只要异步任务有了运行结果,就在’‘任务队列’'之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会依次读取任务队列中的异步任务,被读取的异步任务结束等待状态,进入执行栈,开始执行。主线程从"任务队列"中读取事件的过程是循环往复的,这种执行机制成称为事件循环.
在这里插入图片描述

栈:先进后出

队列:先进先出

链表:通过.next方式串联节点

具体执行顺序:

  1. 主线程运行的时候会生成堆(heap)和栈(stack);
  2. js从上到下解析方法,将其中的同步任务按照执行顺序排列到执行栈中;
  3. 当程序调用外部的API时,比如ajax、setTimeout等,会将此类异步任务挂起,继续执行执行栈中的任务,等异步任务返回结果后,再按照执行顺序排列到事件队列中;
  4. 主线程先将执行栈中的同步任务清空,然后检查事件队列中是否有任务,如果有,就将第一个事件对应的回调推到执行栈中执行,若在执行过程中遇到异步任务,则继续将这个异步任务排列到事件队列中。
  5. 主线程每次将执行栈清空后,就去事件队列中检查是否有任务,如果有,就每次取出一个推到执行栈中执行,这个过程是循环往复的.

从本质上说,进程和线程都是 CPU 工作时间片的一个描述:进程是资源分配的最小单位,线程是CPU调度的最小单位。
一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程是运行在虚拟内存上的,虚拟内存是用来解决用户对硬件资源的无限需求和有限的硬件资源之间的矛盾的。从操作系统角度来看,虚拟内存即交换文件;从处理器角度看,虚拟内存即虚拟地址空间。

宏任务和微任务(5星)

宏任务:setTimeout,setInterval,setImmediate,I/O,UI交互事件,事件绑定
setImmediate该方法用来把一些需要长时间运行的操作放在一个回调函数里,在浏览器完成后面的其他语句后,就立刻执行这个回调函数,

微任务:Promise.then,process.nextTick(node)

同一次事件循环中,微任务永远在宏任务之前执行。

任务执行顺序?(重要)

1.将同步任务排队到执行栈中,异步任务排队到任务队列中。

2.浏览器环境中执行方法时,先将执行栈中的同步任务清空,再将微任务推到执行栈中并清空,之后检查是否存在任务,若存在则取出一个宏任务,执行完成检查是否有微任务,以此循环…

3.Event Loop在浏览器与node环境中的区别:

  • ​ 浏览器环境每次执行一个宏任务,就去检查微任务
  • ​ node会清空当前所处阶段的队列,即执行所有task(宏任务),再去检查微任务,node11版本Event Loop运行原理发生了变化,与浏览器一致.

Promise async/await执行过程(3星)

Promise 和 async 中的立即执行:

Promise 中的异步体现在thencatch中,所以写在 Promise 中的代码是被当做同步任务立即执行的。

而在 async/await 中,在 await 出现之前,其中的代码也是立即执行的。 await+表达式执行完之后才能继续执行函数中其他代码

而await +表达式会先执行一遍,将 await +表达式 后面的代码加入到 微任务中,然后就会跳出整个 async 函数来执行后面的代码。所以 await +表达式后面的代码是微任务.

总结:任务执行,先整体过一遍,同步任务在主线程按顺序执行,异步任务中微任务永远在宏任务之前执行,理清它们进入任务队列的顺序,主线程中的微任务宏任务排在前面。

Object的方法

for…in

主要用于循环对象属性。循环中的代码每执行一次,就会对对象的属性进行一次操作。其语法如下:

var obj = {a: 1, b: 2, c: 3}; 
 
for (var i in obj) { 
    console.log('键名:', i); 
    console.log('键值:', obj[i]); 
}

for in 方法不仅会遍历当前的对象所有的可枚举属性,还会遍历其原型链上的属性。

Object.defineProperty()

Object.defineProperty方法直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象。

//obj要定义属性的对象  
//prop一个字符串或 Symbol,指定了要定义或修改的属性键
//descriptor要定义或修改的属性的描述符。
Object.defineProperty(obj, prop, descriptor)  
  • value: 设置属性的值
  • writable: 值是否可以重写。默认值为false
  • enumerable: 目标属性是否可以被枚举。默认值为false
  • configurable: 目标属性是否可以被删除或是否可以再次修改特性。 默认值为false
  • set:目标属性设置值的方法
  • get:目标属性获取值的方法,当使用了getter或setter方法,不允许使用writable和value这两个属性,如果使用,会直接报错
const o = {}; // 创建一个新对象
// 通过 defineProperty 使用数据描述符添加对象属性的示例
Object.defineProperty(o, "a", {
  value: 37,
  writable: true,
  enumerable: true,
  configurable: true,
});//等价于 o.a=37
// 'a' 属性存在于对象 o 中,其值为 37  o={a:37}  

// 通过 defineProperty 使用访问器属性描述符添加对象属性的示例
let bValue = 38;
Object.defineProperty(o, "b", {
  get() {
    return bValue;
  },
  set(newValue) {
    bValue = newValue;
  },
  enumerable: true,
  configurable: true,
});
o.b; // 38
// 'b' 属性存在于对象 o 中,其值为 38。
// o.b 的值现在始终与 bValue 相同,除非重新定义了 o.b。

// 数据描述符和访问器描述符不能混合使用
Object.defineProperty(o, "conflict", {
  value: 0x9f91102,
  get() {
    return 0xdeadbeef;
  },
});
// 抛出错误 TypeError: value appears only in data descriptors, get appears only in accessor descriptors

Object.prototype.hasOwnProperty()

hasOwnProperty() 方法返回一个布尔值,表示对象自有属性中是否具有指定的属性。而不是继承来的属性

//prop要测试的属性的字符串名称或者Symbol
hasOwnProperty(prop)
const object1 = {};
object1.property1 = 42;

console.log(object1.hasOwnProperty('property1'));
// Expected output: true

console.log(object1.hasOwnProperty('toString'));
// Expected output: false

console.log(object1.hasOwnProperty('hasOwnProperty'));
// Expected output: false

Object.seal () 和 Object.freeze()

Object.freeze() 使一个对象被冻结。被冻结的对象不能再被更改:不能添加新的属性,不能移除现有的属性,不能更改它们的可枚举性、可配置性、可写性或值,对象的原型也不能被重新指定。但是如果一个属性的值是个对象,则这个对象中的属性是可以修改的,除非它也是个冻结对象。

// obj 要冻结的对象。
Object.freeze(obj)
const obj = {
  prop: 42,
};

Object.freeze(obj);

obj.prop = 33;
// Throws an error in strict mode
console.log(obj.prop);
// Expected output: 42

Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要可写就可以改变。

// obj 要密封的对象。
Object.seal(obj)
const object1 = {
  property1: 42,
};

Object.seal(object1);
object1.property1 = 33;
console.log(object1.property1);
// Expected output: 33

delete object1.property1; // Cannot delete when sealed
console.log(object1.property1);
// Expected output: 33

方法的相同点:

  • ES5新增。
  • 对象不可能扩展,也就是不能再添加新的属性或者方法。
  • 对象已有属性不允许被删除。
  • 对象属性特性不可以重新配置。

方法不同点:

  • Object.seal方法生成的密封对象,如果属性是可写的,那么可以修改属性值。
  • Object.freeze方法生成的冻结对象,属性都是不可写的,也就是属性值无法更改。

Object.prototype.toString()

Object.prototype.toString()可以返回当前调用者的对象类型。

const theDog = new Dog("Gabby", "Lab", "chocolate", "female");
theDog.toString(); // "[object Object]"

关于Object.prototype.toString().call()判断数据类型为什么加.call()
在Object.prototype.toString()中,他的调用者永远都是Object.prototype,在不加call()情况下,我们的出来的结果永远都是 ‘[object Object]’
call()是为了改变Object.prototype.toString这个函数都指向。让Object.prototype.toString这个方法指向我们所传入的数据。

const toString = Object.prototype.toString;

Object.prototype.toString.call(new Date()); // [object Date]
Object.prototype.toString.call(new String()); // [object String]
// Math has its Symbol.toStringTag
Object.prototype.toString.call(Math); // [object Math]
Object.prototype.toString.call(undefined); // [object Undefined]
Object.prototype.toString.call(null); // [object Null]

Object.keys()、Object.values()、Object.entries()

这三个方法都用来遍历对象,它会返回一个由给定对象的自身可枚举属性(不含继承的和Symbol属性)组成的数组,数组元素的排列顺序和正常循环遍历该对象时返回的顺序一致,这个三个元素返回的值分别如下:

Object.keys():返回包含对象键名的数组;
Object.values():返回包含对象键值的数组;
Object.entries():返回包含对象键名和键值的数组。

let obj = { 
  id: 1, 
  name: 'hello', 
  age: 18 
};
console.log(Object.keys(obj));   // 输出结果: ['id', 'name', 'age']
console.log(Object.values(obj)); // 输出结果: [1, 'hello', 18]
console.log(Object.entries(obj));   // 输出结果: [['id', 1], ['name', 'hello'], ['age', 18]

注意

  • Object.keys()方法返回的数组中的值都是字符串,也就是说不是字符串的key值会转化为字符串。
  • 结果数组中的属性值都是对象本身可枚举的属性,不包括继承来的属性。

ES6

变量和函数怎么进行提升的?优先级是怎么样的?(4星)

在ES6之前,JavaScript没有块级作用域(一对花括号{}即为一个块级作用域),只有全局作用域和函数作用域。
变量提升:变量的声明会被提升到当前作用域的最上面,变量的赋值不会提升,变量提升就是在变量创建之前可以使用(比如输出:输出的是默认值undefined),let不存在,var存在

  • 对变量进行提升,只声明,不赋值,值为undefined

函数提升:函数的声明会被提升到当前作用域的最上面,但不会调用函数

  • 开辟堆空间

  • 存储内容

  • 将地址赋给变量

  • 函数提升优先级高于变量提升

 //变量提升:变量的声明会被提升到当前作用域的最上面,变量的赋值不会提升
    var fn = function () {
      console.log(1);
    }
    fn();
    //=> var fn;
    // fn();
    // fn=function(){
    //   console.log(1);
    // }
    //函数提升:函数的声明会被提升到当前作用域的最上面,但不会调用函数
    fn();

    function fn() {
      console.log(1);
    }
    //=> function fn() {
    //   console.log(1);
    // }
    // fn();

 //案例1
 function Foo() {
      getName = function () {
        console.log(1);
      }
      return this;
    }
    Foo.getName = function () {
      console.log(2);
    }
    Foo.prototype.getName = function () {
      console.log(3);
    }
    var getName = function () {
      console.log(4);
    }

    function getName() {
      console.log(5)
    }
    //变量提升
    //[1]function Foo() {
    //   getName = function () {
    //     console.log(1);
    //   }
    //   return this;//window
    // }
    //[2]var getName;
    //[3]function getName() {
    //   console.log(5)
    // }
    //[4]Foo.getName = function () {
    //   console.log(2);
    // }
    //[5]Foo.prototype.getName = function () {
    //   console.log(3);
    // }
    //[6]getName= function () {
    //   console.log(4);
    // }
//运算类型优先级 new(带参数列表)>new(无参数列表))
    Foo.getName(); //2 [4]Foo.getName()就是单纯的表示 输出函数 Foo的静态方法,所以直接输出2
    getName(); //4 [6] getName()因为提前声明的原因, 声明后被  var getNmae() = xxx 所覆盖  所以这里的输出 变成 4
    Foo().getName(); //1 先执行Foo(),里面的return this 是window,即Foo().getName()变成window.getName 在Foo里面的全局变量getName覆盖,所以输出为 1
    getName(); //1 同上 1
    new Foo.getName();//2 [4]new  18 Foo.getName() 19 ,先Foo.getName()在new,为2
    new Foo().getName(); //3 [5]new Foo() 19 Foo.getName() 19 优先级相同从左到右,先new Foo()在getName(),new Foo()创建实例,xxx.getName()原型上的方法 为3
    new new Foo().getName(); //3 [5]先new Foo().getName()  new(new Foo().getName)new Foo()创建实例,xxx.getName()原型上的方法 为3

var let const 有什么区别? (5星)

  • var 声明变量存在变量提升,let 和 const 不存在 声明对象类型使用 const,非对象类型声明选择 let;
  • var会与window相映射(会挂一个属性),而let不与window相映射
  • var 的作用范围是函数作用域,而let、const 的作用范围是块级作用域(局部变量)
  • 同一作用域下 var可以重复声明变量,而 let const 可以
  • 同一作用域下在 let 和 const 声明前使用会存在暂时性死区,即在let、const声明之前,变量不可以使用
  • const
    • const定义常量一旦声明必须赋值,不允许重复声明,声明后的常量(内存地址)不能再修改
    • 使用const后,确保该变量的引用地址不变,可以修改内部的属性。
    • 对于基本数据类型(如数字、字符串、布尔值),const确保变量的值不会改变。
    • 对于对象,仍然可以修改对象的属性,但不能重新赋值整个对象。
    • 对于数组,你可以修改、添加或删除元素,但不能重新赋值整个数组。

const只能使引用类型的地址不改变,但如何使引用类型对应的值也不改变呢?

方法一:
Object.defineProperty() 静态方法会直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象。
Object.defineProperty(obj, prop, descriptor)  obj要定义属性的对象  prop一个字符串或 Symbol,指定了要定义或修改的属性键 descriptor要定义或修改的属性的描述符
const object1 = {
 "property1": 123
};

Object.defineProperty(object1, 'property1', {
  writable: false // 设置不允许写入
});

object1.property1 = 77;
console.log(object1);
// 输出:{property1: 123}

方法二:
Object.freeze() 使一个对象被冻结。被冻结的对象不能再被更改:不能添加新的属性,不能移除现有的属性,不能更改它们的可枚举性、可配置性、可写性或值,对象的原型也不能被重新指定。
但是如果一个属性的值是个对象,则这个对象中的属性是可以修改的,除非它也是个冻结对象。

const object1 = {
 "property1": 123,
 "property2": {
     'key1': 456   
  }
};
Object.freeze(object1); // 冻结

object1.property1 = 111; // 不会被修改
object1.property2 = 222; // 不会被修改

object1.property3 = 333; // 不会新增
// object1.property3 // undefined

object1.property2.key1 = 666 // 会改变
// object1.property2.key1 // 666

const propKey1 = Object.freeze({'key1': 456})
const object1 = {
 "property2": propKey1 // 对象属性也是冻结对象
};
Object.freeze(object1);

object1.property2.key1 = 666 // 不会被改变
// object1.property2.key1 // 456



方法三:
Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要可写就可以改变。


const object1 = {
  property1: 42,
};

Object.seal(object1);
object1.property1 = 33;
console.log(object1.property1);
// Expected output: 33

delete object1.property1; // Cannot delete when sealed
console.log(object1.property1);
// Expected output: 33

如何使引用类型的所有值都不能修改?
通过freeze()封装的深冻结函数使对象为静态的

// 深冻结函数.
function deepFreeze(obj) {
  // 取回定义在obj上的属性名
  //Object.getOwnPropertyNames() 静态方法返回一个数组,其包含给定对象中所有自有属性(包括不可枚举属性,但不包括使用 symbol 值作为名称的属性)。
   var propNames = Object.getOwnPropertyNames(obj);
  // 在冻结自身之前冻结属性
  propNames.forEach(function(name) {
    var prop = obj[name];
    // 如果prop是个对象,冻结它
    if (typeof prop == 'object' && prop !== null)
      //递归
      deepFreeze(prop);
  });
  // 冻结自身(no-op if already frozen)
  return Object.freeze(obj);
}
const obj2 = {
  internal: { key: 123 }
};
deepFreeze(obj2);

箭头函数,以及与普通函数的区别,箭头函数可以当做构造函数 new吗?箭头函数的作用(5星)

箭头函数()=>{}:

  1. 如果形参只有一个,则小括号可以省略;
  2. 函数体如果只有一条语句,则花括号可以省略,函数的返回值为该条语句的执行结果;

与普通函数的区别

  1. 普通函数有函数提升,而箭头函数没有
  2. 箭头函数没有属于自己的thisarguments,箭头函数的this指向为父级(往外层作用域找):首先从它的父级作用域中找,如果父级作用域还是箭头函数,再往上找,直到找到this的指向。
  3. 箭头函数不能作为构造函数,也就是说,不可以使用new命令(没有this),否则会抛出一个错误。
  4. 没有 yield 属性,不能用作生成器 Generator 函数

箭头函数不能new

  • 没有自己的this,不能调用call和apply
  • 没有prototype,new关键字内部需要把新对象的_proto_指向函数的prototype

箭头函数的作用:

1.语法简洁

2.可以隐式返回

//显示返回
const double3 = numbers.map(number => {
    return number * 2;  
})
//隐式返回
//去掉return和花括号括号,把返回内容移到一行,较为简洁;
const double3 = numbers.map(number => number * 2);

3.不绑定this

this实际上是继承自其父级作用域中的this,箭头函数本身的this是不存在的,这样就相当于箭头函数的this是在声明的时候就确定了,this的指向并不会随方法的调用而改变

Promise(什么是Promise?)

Promise :ES6引入的异步编程的新解决方案,语法是一个构造函数 ,用来封装异步操作并可以获取其成功或失败的结果. Promise对象有三种状态:初始化pending 成功fulfilled 失败rejected

  • Promise 就是一个对象,用来表示并传递异步操作的最终结果
  • Promise {: PromiseResult} PromiseState:状态 PromiseResult:返回值
  • Promise 最主要的交互方式:将回调函数传入 then 方法来获得最终结果或出错原因
  • Promise 代码书写上的表现:以“链式调用”代替回调函数层层嵌套(回调地狱)p.then().then()

当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的。Promise构造函数是主线程,而Promise.then是微任务. 调用resolve 这个promise对象的状态为成功,调用reject 这个promise对象的状态为失败

new Promise((resolve, reject) => {
  console.log('new Promise');
  //状态为成功
  resolve('success');
});
console.log('finifsh');

// 先打印new Promise, 再打印 finifsh
// 创建 Promise 对象
// Promise 对象三种状态:初始化、成功、失败  参数为异步函数
const p = new Promise((resolve,reject) => {
setTimeout(() => {
resolve("用户数据");// 调用resolve,Promise对象的状态就会变成成功
//reject("失败了");//调用reject,Promise对象的状态就会变成失败
},1000);
});

// Promise对象的then方法用来指定回调,它的两个参数均为函数
p.then(value => console.log(value)// 若promise的状态为成功,则执行then方法中的第一个参数 
  //p的状态为成功 打印 "用户数据"
, reason => console.log(reason)// 若状态为失败,则执行then方法中的第二个参数
);

// then方法的返回结果result是promise对象,该promise对象的状态由回调函数p.then的结果决定;
const result = p.then(value => {
// 1、如果回调函数中返回的结果是 非promise类型的数据
// 状态为成功,返回值为对象的成功值
// 34行代码打印[[PromiseStatus]]:"resolved" [[PromiseValue]]:123 (返回值) 
    return 123
    
// 2、如果回调函数中的返回结果是是promise类型的数据 此Promise对象的状态决定上面Promise对象p的状态
// return new Promise((resolve,reject)=>{
// // resolve("ok"); //34行代码打印 [[PromiseStatus]]:"resolved" [[PromiseValue]]:"ok"
// reject("error"); //34行代码打印 [[PromiseStatus]]:"rejected" [[PromiseValue]]:"error"
// });
    
// 3、抛出错误
// throw "失败啦!"; //34行代码打印 [[PromiseStatus]]:"rejected" [[PromiseValue]]:"失败啦!"
},reason => {
console.error(reason);
})
console.log(result);//34行
// 链式调用   then里面两个函数参数,可以只指定一个
p.then(value=>{},reason=>{}).then(value=>{},reason=>{});

​ Promise 实现了链式调用,也就是说每次调用 then 之后返回的都是一个 Promise,并且是一个全新的 Promise,原因也是因为状态不可变。如果你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装。 Promise.resolve()将对象转换为promise对象

Promise.resolve(1)
  .then((res) => {
    console.log(res); // => 1
    return 2; // 包装成 Promise.resolve(2)
  })
  .then((res) => {
    console.log(res); // => 2
  });

Promise 也很好地解决了回调地狱的问题.

回调地狱:`回调函数层层嵌套,代码可读性低、不方便后期维护

ajax(url)
  .then((res) => {
    console.log(res);
    return ajax(url1);
  })
  .then((res) => {
    console.log(res);
    return ajax(url2);
  })
  .then((res) => console.log(res));
//手写一个Promise
const promise = new Promise(function (resolve, reject) {
  if (true) {
    resolve(value)
  } else {
    reject(error)
  }
})
promise.then(res => {
  console.log(res)
}).catch(err => {
  console.log(err)
})

100个HTTP请求如何快速请求完,限制最大并发数10个

  //思路:维护一个运行池一个等待队列,出一个进一个,控制运行池的大小
  const urlList = new Array(100).fill('xxx');
  const pool = new Set();
  const waitOueue: any[] = [];
  const request = (url) => {
    return new Promise((res, rej) => {
      const isFull = pool.size >= 10;
      const fn = function fn() {
        const request = fetch(url);
        request.finally(() => {
          pool.delete(fn);
          const next = waitOueue.shift();
          next && pool.add(next);
          setTimeout(() => next?.());
        });
        request.then(res);
        request.catch(rej);
        return request;
      };
      if (isFull) {
        waitOueue.push(fn);
      } else {
        pool.add(fn);
        fn();
      }
    });
  };
  for (let [index, url] of urlList.entries()) {
    request(url).then(() => {
      console.log(index);
    });
  }

Promise.all、Promise.any、Promise.race、promise.allsettled的区别(5星)

promise.all 只有当所有的promise执行返回成功才会返回成功,有一个返回失败就会返回失败

promise.any 和promise.all相反,当所有的promise执行返回失败才会返回失败,有一个返回成功就会返回那个成功的promise

promise.race 赛跑机制 返回成功或失败都依赖于第一个执行完毕的promise返回成功则成功,失败则返回失败

promise.allsettled 当所有的promise执行完毕,返回一个数组中存放每一个promise的结果

一、二、三级分类,作为详情页分开字段的数据回显,不需要关心他们之间的关联关系,一个获取不到可以显示为空,这种情况用 Promise.allSettled

一、二、三级分类,作为编辑页的下拉框的级联选择项,这种情况需要三个列表都获取完毕,且没有 reject,这种情况用 Promise.all

async和await(3星)

asyncawait两种语法结合可以让异步代码看起来像同步代码一样,简化异步函数的写法

async函数:

1.async函数的返回值为promise对象;

2.promise对象的结果由async函数执行的返回值决定

3.async 是Generator函数的语法糖,并对Generator函数进行了改进,是对 yield 的简单封装

    async function fn() {
      //1.如果返回结果是 非promise对象,状态为成功,返回值为对象成功值fulfilled
      return 123;
      //2.抛出错误
      throw "出错啦";
    }
    const result = fn();
    console.log(result); // Promise {<fulfilled>: 123} Promise {<rejected>: '出错啦'}
-----------------------------------------------------------------------------------------
    async function fn() {
      //3.如果返回的promise对象,那么返回的最终结果就是Promise对象
      return new Promise((reslove, reject) => {
        //resolve('成功');
        reject('失败');
      })
    }
    const result = fn();
    console.log(result); //Promise {<rejected>: '失败'}
    // then方法来获得最终结果或出错原因
    result.then(
      (value) => {
        console.log(value);
      },
      (reason) => {
        console.log(reason); //失败
      }
    )

await表达式:

1.await必须写在aysnc函数中;

2.await后面的表达式一般为Promise对象;

3.await返回的是Promise成功的值;

4.await的Promise失败了,就会抛出异常,无法处理promise返回的reject对象,需要通过try…catch捕获处理.

    //async函数 + await表达式:异步函数
    async function fn() {
      //await 返回的promise成功值
      let result = await new Promise((resolve, reject) => {
      resolve('成功')
    });
      console.log(result); //成功
    }
    fn();

一个await 的例子:

let a = 0;
let b = async () => {
  a = a + (await 10);
  console.log('2', a);
};
b();
a++;
console.log('1', a);

//先输出  ‘1’, 1
//在输出  ‘2’, 10
  • 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为 await 内部实现了 generator ,generator 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
  • 因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码
  • 同步代码 a++ 与打印 a 执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10

上述解释中提到了 await 内部实现了 generator,其实 await 就是 generator 加上 Promise 的语法糖,且内部实现了自动执行 generator

异步实现方式及各自方式的优缺点(4星)

方式一:回调函数(callback):将一个函数当做参数传到另一个函数里,当那个函数执行完后,再执行传进去的这个函数,这个过程就叫做回调

回调地狱:回调函数层层嵌套,层层依赖代码可读性低、不方便后期维护

优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。)

缺点:回调地狱,每个任务只能指定一个回调函数,不能 return.

方式二: Promise

Promise就是为了解决callback的问题而产生的。

Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装

优点:解决了回调地狱的问题

缺点:无法取消 Promise ,内部抛出的错误需要通过回调函数来捕获

方式三:生成器Gnenrator

Generator是解决异步编程的一种方案,解决回调地狱

方式四:async/await

优点:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题

缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。

解决异步回调地狱的方式(3星)

Promise、generator、asynac/await

map、forEach、for、for in、for of的区别(5星)

map不改变原数组但是会返回新数组,可以使用break中断循环,可以使用return返回到外层函数
forEach 不改变原数组也没有返回值,不能使用break中断循环,不能使用return返回到外层函数
map()跟forEach有什么区别
map()

  • 返回一个回调函数处理后的新数组
  • 对原数组没有影响
  • 目的:处理数组数据
    forEach
  • 没有返回值,不会生成新数组
  • 无法修改原数组
  • 目的:遍历数组
    for 循环用于遍历数组和字符串等数据结构。for可以用continue跳过循环中的一个迭代
    for...in 遍历对象键值(key),或者数组下标,for…in 循环遍历的是对象的可枚举属性,包括对象自身的属性以及从原型链继承的属性。若只需遍历对象自身的属性而不包括继承的属性,可以使用 hasOwnProperty() 方法进行过滤
    for...of 遍历可迭代对象 (如数组、字符串、Set、Map 等) 中的元素,遍历键值对的值,for…of 循环不适用于普通对象(Plain Object),因为普通对象不是可迭代对象。若需要遍历普通对象的属性,可以使用 for…in 循环。
    在以上的循环中如果需要终止后续的程序或者终止当前的循环可以使用到以下的方法: return break continue
    let arr = ['apple', 'orange']
    for(let v of arr) {
        if(v == 'apple') {
            continue;//跳出当前的循环,并不会终止;
        }
        return false;//终止循环 如果for循环中嵌套了for循环也将会终止
        break;//用于跳出最近的一次循环,如果for循环中嵌套了for循环嵌套的循环将会继续执行
    }

解构赋值(3星)

ES6中允许从数组中提取值,按照对应位置,对变量赋值。对象也可以实现解构
剩余参数语法允许我们将一个不定数量的参数表示为一个数组。

//...args ...表示接受剩余的参数
function sum(first, ...args) {
  console.log(first);//10
  console.log(args);//[20,30]
}
sum(10, 20, 30);

数组解构

let [a, b, c] = [1, 2, 3]   //a=1, b=2, c=3
let [d, [e], f] = [1, [2], 3]    //嵌套数组解构 d=1, e=2, f=3
let [g, ...h] = [1, 2, 3]   //数组拆分 g=1, h=[2, 3]
let [i,,j] = [1, 2, 3]   //不连续解构 i=1, j=3
let [k,l] = [1, 2, 3]   //不完全解构 k=1, l=2

对象解构

let {a, b} = {a: 'aaaa', b: 'bbbb'}      //a='aaaa' b='bbbb'
let obj = {d: 'aaaa', e: {f: 'bbbb'}}
let {d, e:{f}} = obj    //嵌套解构 d='aaaa' f='bbbb'
let g;
(g = {g: 'aaaa'})   //以声明变量解构 g='aaaa'
let [h, i, j, k] = 'nice'    //字符串解构 h='n' i='i' j='c' k='e'

事件代理/事件委托(3星)

指不在事件的触发目标上设置监听,而是在其父元素设置,通过冒泡机制,父元素可以监听到子元素上事件的触发。

为什么要使用模块化?

都有哪几种方式可以实现模块化,各有什么特点?(3星)

  • 模块化是指将一个大的程序文件,拆分成许多小的文件,然后将小文件组合起来.

    一个模块是实现一个特定功能的一组方法

    优点:1. 防止命名冲突;2. 代码复用;3. 高维护性;

    实现模块化方式

    • AMD
    • CMD
    • CommonJS模块
    • ES6 模块

模块加载方案比较:CommonJS模块 与 ES6模块(3星)

CommonJS模块,它通过 require 来引入模块,通过 module.exports 定义模块的输出接口。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题。

ES6模块使用 import 和 export 的形式来导入导出模块。

区别:

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。CommonJS 模块输出的是值的拷贝,模块内部的变化就影响不到这个值。ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口 对外接口只是一种静态定义,在代码静态解析阶段就会生成。CommonJS 模块就是对象,即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

扩展运算符

扩展运算符可以将数组或者对象转为用逗号分隔的参数序列。

let ary = [1, 2, 3];
...ary //1,2,3
console.log(...ary);//1 2 3

扩展运算符可以应用于合并数组。

let ary1 = [1, 2, 3];
let ary2 = [4, 5, 6];
ary1.push(...ary2);
console.log(ary1);//[ 1, 2, 3, 4, 5, 6 ]

跨域

同源策略

如果两个页面(接口)的协议、域名、端口号都相同,我们认为它们为同源
同源策略就是浏览器的一个安全限制,它阻止了不同【域】之间进行的数据交互

跨域的方式都有哪些?他们的特点是什么? (5星)

跨域是指一个域下的文档或脚本试图去请求另一个域下的资源
同源策略是 浏览器 为了安全而作出的限制策略(所以服务端不涉及到跨域),同源政策是浏览器给予Ajax技术的限制,无法向非同源地址发送Ajax 请求,服务器端是不存在同源政策限制。 浏览器请求必须遵循同源策略,即同域名、同端口、同协议

当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。

(1)CORS技术 : 现在主流解决方案, 不需要前端写任何代码
方案:服务器只需要设置一个允许跨域响应头即可
res.setHeader(‘Access-Control-Allow-Origin’, ‘*’)
(2)jsonp技术 : 以前的方案,现在很少使用。
JSONP

  • JSONP原理:就是通过script标签获取后端接口发送过来的数据通过js实现,但是jsonp只能解决get请求,这也是它的一个缺点。而且也不安全 json劫持:攻击者可以构造恶意的JSONP调用页面,诱导被攻击者访问来截取用户敏感信息

  • JSONP只能get请求

  • 源码:

        function addScriptTag(src) {
          var script = document.createElement("script")
          script.setAttribute('type','text/javascript')
          script.src = src
          document.appendChild(script)
        }
    
        // 回调函数
        function endFn(res) {
          console.log(res.message);
        }
        // 前后端商量好,后端如果传数据的话,返回`endFn({messag:'hello'})`
    

CORS

CORS(跨域资源共享)原理:它允许浏览器向跨源服务器,发出请求,从而克服了AJAX只能同源使用的限制。实现原理:在服务器返回响应的时候给响应头设置一个允许跨域的header,把Access-Control-Allow-Origin指定为可获取数据的域名
CORS请求分为两类:简单请求 非简单请求。
简单请求,浏览器在请求头中增加一个Origin字段直接发出CORS请求
非简单请求,会在正式通信之前,增加一次HTTP查询请求,浏览器先询问服务器,只有得到服务器的肯定答复时,浏览器才会发出正式请求,否则就报错。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
nginx反向代理跨域
把 客户端和nginx反向代理服务器放在同一端口上,这样就不会存在同源限制,而 nginx模拟一个虚拟服务器,服务器与服务器之间是不存在跨域的,就能把客户端http请求转发到另一个或者一些服务器上,从而轻松实现跨域访问

  • 发送数据时 ,客户端->nginx->服务端
  • 返回数据时,服务端->nginx->客户端
    正向代理 是一个位于客户端和目标服务器之间的代理服务器,客户端为了从目标服务器取得内容,向代理服务器发送一个请求,然后代理服务器转交请求给目标服务器并将获得的内容返回给客户端
    对于客户端来说,反向代理就好像目标服务器。客户端向反向代理发送请求,接着反向代理判断请求走向何处,并将请求内容转交给客户端,使得这些内容就好似他自己一样,客户端并不会感知到反向代理后面的服务,只需要把反向代理服务器当成真正的服务器就好了。

我们常说的代理也就是指正向代理,正向代理的过程,它隐藏了真实的请求客户端,服务端不知道真实的客户端是谁,客户端请求的服务都被代理服务器代替来请求。
反向代理隐藏了真实的服务端,当我们请求一个网站的时候,背后可能有成千上万台服务器为我们服务,但具体是哪一台,我们不知道,也不需要知道,我们只需要知道反向代理服务器是谁就好了,反向代理服务器会帮我们把请求转发到真实的服务器那里去。反向代理器一般用来实现负载平衡(尽量别说)。

DOM

DOM绑定事件的几种方式及区别(3星)

  • element.onclick,元素+事件类型

对同一个元素绑定多次同类事件,写在后面的会覆盖前边的事件

  • addEventListener方法
    btn.addEventListener(type, handle, true)三个参数分别为事件类型,事件处理程序,执行的阶段;
    第三个参数有两个值,true和false,false默认值,事件冒泡阶段执行,true事件捕获阶段执行

对同一个元素绑定多次同类事件,不会发生覆盖,会顺序执行所绑定的事件

DOM事件流(5星)

在这里插入图片描述

getElementById querySelector querySelectorAll获取元素

两种事件流都会触发 DOM 的所有对象,从 window 对象开始,也在 window 对象结束。

DOM 标准规定事件流包括三个阶段:

  • 事件捕获阶段
  • 处于目标阶段
  • 事件冒泡阶段

捕获和冒泡阶段事件流都会触发 DOM 的所有对象,从 window 对象开始,也在 window 对象结束。

事件捕获和事件冒泡有什么区别?(5星)

  • 事件捕获

    • 从上至下(祖宗到儿子)执行
    • 在addEventListener中的第三属性设置为true
  • 事件冒泡 事件代理/事件委托

    • 从下至上(儿子至祖宗)执行
    • 在addEventListener中的第三属性设置为false(默认)

杂项

this 的指向有哪几种情况?(5星)

this 代表函数调用相关联的对象,通常也称之为执行上下文。

  1. 作为函数直接调用,非严格模式下,this 指向 window,严格模式下,this 指向 undefined;
  2. 作为某对象的方法调用,this 通常指向调用的对象。
  3. apply、call、bind 可以绑定 this 的指向。
  4. 在构造函数中,this 指向新创建的对象
  5. 箭头函数没有单独的 this 值,this 在箭头函数创建时确定,箭头函数的this指向为父级:首先从它的父级作用域中找,如果父级作用域还是箭头函数,再往上找,直到找到this的指向。

什么是防抖?什么是节流?手写一个(5星)

防抖节流都是控制事件触发频率的方法
防抖 只会在最后一次触发结束后等待 规定时间 执行
节流 频繁触发 run,只会每隔 规定 时间执行一次。
防抖:
搜索框搜索输入。只需用户最后一次输入完,再发送请求
节流:
滚动加载,加载更多或滚到底部监听

  //防抖函数 重在清零 clearTimeout(timer)
  //定时器期间,有新操作时,清空旧定时器,重设新定时器
function debounce(fn, time)  {
//先声明一个计时器
	let timer
	return function() {
	//如果计时器存在则清空
		if(timer) clearTimeout(timer)
		//否则重设新定时器
		timer = setTimeout(() => {
			fn.call(this)
		}, time)
	}
}



  // 节流  重在开关锁 timer=null 利用定时器来保证间隔时间内事件的触发次数
//传入两个值,一个是要执行的方法,一个是执行周期
function throttle(fn,time) {
	//先声明一个定时器
	let timer
	//返回一个方法(闭包)
	return function() {
		//如果定时器存在则不执行
		if(timer) return
		//如果定时器不存在则执行
		//设置一个一定时间自身清理的计时器
		timer = setTimeout(() => {
			//此this到时候会变成调用这个方法的元素
		    fn.call(this)
			timer = null
		},time)
	}
}

函数柯里化原理(5星)

//函数柯里化,就是可以将一个接受多个参数的函数分解成多个接收单个参数的函数的技术,直到接收的参数满足了原来所需的数量后,才执行原函数
//简单为用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数
//降低代码的重复,提高代码的适用性
const curry = (fn, ...args) => fn.length <= args.length // 判断参数的数量足够执行函数
  	? fn(...args) // 参数足够执行函数
    : curry.bind(null, fn, ...args) // 参数不够时返回传入已有参数,使用bind结合闭包思想返回一个临时函数
        //fn(this,1,2) 改变fn中的this,fn并不执行 等待事件触发
// 检验
function sum (a, b, c) { return a + b + c }
let currySum = curry(sum)

console.log( currySum(1, 2, 3) ) // 6//以 currySum(1, 2, 3)为例,可以用一个变量保存curry(1, 2),然后每次只传入第三个参数,相当于前两个参数给定了

js常见的设计模式(5星)

设计模式是一套被反复使用,多数人知晓的,经过分类的,代码设计经验的总结。

  • 单例模式、工厂模式、代理模式
    单例模式
    概念:同一个构造函数,生成唯一的实例,防止重复的实例化
    优点:节省内存空间,提高程序执行效率

1.只有一个实例。2.可以全局访问。3.节省内存

var Single = (function () {
      var instance = null
      function Single(name) {
        this.name = name
      }
      return function (name) {
        if (!instance) {
          instance = new Single()
        }
        return instance
      }
    })()

    var oA = new Single('hi')
    var oB = new Single('hello')
    console.log(oA);
    console.log(oB);
    console.log(oB === oA);

工厂模式

代替new创建一个对象,且这个对象像工厂制作一样,批量制作属性相同的实例对象(指向不同)

 function Animal(o) {
      var instance = new Object()
      instance.name = o.name
      instance.age = o.age
      instance.getAnimal = function () {
        return "name:" + instance.name + " age:" + instance.age
      }
      return instance
    }

    var cat = Animal({name:"cat", age:3})
    console.log(cat);

代理模式
在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在两个对象之间起到中介的作用。

JS性能优化的方式(5星)

  • 垃圾回收
  • 闭包中的对象清除
  • 防抖节流
  • 分批加载(setInterval,加载10000个节点)
  • 事件委托
  • script标签中的defer和async
  • CDN

图片的预加载和懒加载

  • 预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染
  • 懒加载:延时加载,即当对象需要用到的时候再去加载
    懒加载的主要目的是作为服务器前端的优化,减少请求数
懒加载原理通过img中的src属性,根据可视化区域对数据进行切割

两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。预加载则会增加服务器前端压力,懒加载对服务器有一定的缓解压力作用。

cookie、session、token、jwt的区别

回答:
Cookie 由于HTTP协议是无状态的,而服务器端的业务必须是要有状态的。Cookie的产生是为了存储web中的状态信息,以方便服务器端使用。 cookie 存储在客户端,cookie 是不可跨域的

session 是基于 cookie 实现的 是另一种记录服务器和客户端会话状态的机制

token是 访问资源接口(API)时所需要的资源凭证
Token 和 cookie、session的区别

  • cookie 在客户端中存放,容易伪造,不如 session 安全
  • session 会消耗大量服务器资源,cookie 在每次 HTTP 请求中都会带上,影响网络性能
  • 由于 session 存储于服务器端,当用户量很大时会有一定的扩展性问题。token存储在客户端,不存在这个问题
  • Cookie是不允许跨域访问的, token可跨域
  • 请求中发送token能够防止CSRF(跨站请求伪造),cookie不可

认证(鉴权)是指验证用户是否拥有访问系统的权利,就是验证当前用户的身份,证明“你是你自己”,如用户名密码登录,当你的信息和数据库匹配时就会登录成功

授权是用户授予第三方应用访问该用户某些资源的权限

凭证是实现认证和授权的前提,需要一种媒介(证书)来标记访问者的身份

JWT

  • JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。是一种认证授权机制

  • JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。

  • JWT由三部分组成:第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).

    jwt 有三部分组成:A.B.C

    A:Header,{“type”:“JWT”,“alg”:“HS256”} 固定

    B:playload,存放信息,比如,用户id,过期时间等等,可以被解密,不能存放敏感信息

    C: 签证,A和B加上秘钥 加密而成,只要秘钥不丢失,可以认为是安全的。

    jwt 验证,主要就是验证C部分 是否合法

    使用: 在HTTP 请求头信息的 Authorization 字段里,使用 Bearer 模式添加 JWT,授权携带token信息

    • 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为
    • 因为 JWT 是自包含的(内部包含了一些会话信息),因此减少了需要查询数据库的需要
    • 因为 JWT 并不使用 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)
    • 因为用户的状态不再存储在服务端的内存中,所以这是一种无状态的认证机制

cookie、session的区别

  • cookie 储存在在客户端,session 储存在于服务端。 session 是基于 cookie 实现的,session 存储在服务器端
  • cookie 在客户端中存放,容易伪造,不如 session 安全
  • session 会消耗大量服务器资源,cookie 在每次 HTTP 请求中都会带上,影响网络性能

Token 和 JWT 的区别

相同:

  • 都是访问资源的令牌
  • 都可以记录用户的信息
  • 都是使服务端无状态化
  • 都是只有验证成功后,客户端才能访问服务端上受保护的资源

区别:

  • Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。
  • JWT:将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。

cookie,sessionStorage 和 localStorage 的区别?

  • 在同一浏览器下生命周期不同

Cookie生命周期: 默认是关闭浏览器后失效, 但是也可以设置过期时间

SessionStorage生命周期: 仅在当前会话(窗口)下有效,关闭窗口或浏览器后被清除, 不能设置过期时间

LocalStorage生命周期: 除非被清除,否则永久保存

  • 容量不同

Cookie容量限制: 大小(4KB左右)和个数(20~50)

SessionStorage和LocalStorage容量限制: 大小(5M左右)

  • 网络请求不同

Cookie网络请求: 每次都会携带在HTTP请求头中,如果使用cookie保存过多数据会带来性能问题

SessionStorage和LocalStorage网络请求: 仅在浏览器中保存,不参与和服务器的通信
应用场景:localStorage 适合持久化缓存数据,比如页面的默认偏好配置等;sessionStorage 适合一次性临时数据保存。

ajax

什么是ajax?创建过程

ajax是一种异步通信的方法,从服务端获取数据,达到局部刷新页面的效果。Ajax 相当于客户端发送请求与接收响应的代理人,以实现在不影响用户浏览页面的情况下,局部更新页面数据,从而提高用户体验。
过程:
创建XMLHttpRequest对象;
调用open方法传入三个参数 请求方式(GET/POST)、url、同步异步(true/false);
监听onreadystatechange事件,当readystate等于4时返回responseText;
调用send方法传递参数。

同步和异步的区别

同步:
浏览器访问服务器请求,用户看得到页面刷新,重新发请求,等请求完,页面刷新,新内容出现,用户看到新内容,进行下一步操作

异步:
浏览器访问服务器请求,用户正常操作,浏览器后端进行请求。等请求完,页面不刷新,新内容也会出现,用户看到新内容

ajax和axios的区别(5星)

ajax技术实现了网页的局部数据刷新,是客户端发送请求与接收响应的代理人,axios是通过promise实现对ajax技术的一种封装,安装axios依赖用于发起ajax请求

ajax:本身是针对MVC的编程,不符合现在前端MVVM的浪潮
axios:从 node.js 创建 http 请求 支持 Promise API 客户端支持防止CSRF 提供了一些并发请求的接口

ajax的优点和缺点

ajax的优点

1、无刷新更新数据(在不刷新整个页面的情况下维持与服务器通信)

2、异步与服务器通信(使用异步的方式与服务器通信,不打断用户的操作)

3、前端和后端负载均衡(将一些后端的工作交给前端,减少服务器与宽度的负担)

4、界面和应用相分离(ajax将界面和应用分离也就是数据与呈现相分离)

ajax的缺点

1、ajax不支持浏览器back按钮

2、安全问题 Aajax暴露了与服务器交互的细节

3、对搜索引擎的支持比较弱

4、破坏了Back与History后退按钮的正常行为等浏览器机制

JS置对象 String Array Math Date常见方法

String 字符串常见方法:

 str.length:字符串的长度
 str.charAt(索引),返回值是指定索引位置的字符串,超出索引,结果是空字符串
 str.concat(字符串1,字符串2,...);返回的是拼接之后的新的字符串
 str.indexOf(要找的字符串,从某个位置开始的索引);返回的是这个字符串的索引值,没找到则返回-1
 str.lastIndexOf(要找的字符串);从后向前找,但是索引仍然是从左向右的方式,找不到则返回-1
 str.replace(原来的字符串,新的字符串):用来替换字符串的
 str.slice(开始的索引,结束的索引); 提取字符串的片断,并在新的字符串中返回被提取的部分,从索引5的位置开始提取,到索引为10的前一个结束,没有10,并返回这个提取后的字符串
 str.split()把字符串分割为字符串数组
 "hello".split("")	//可返回 ["h", "e", "l", "l", "o"]
 str.substr(开始的位置,个数);返回的是截取后的新的字符串
 str.substring(开始的索引,结束的索引),返回截取后的字符串,不包含结束的索引的字符串
 str.toLocaleLowerCase();转小写
 str.toLowerCase();转小写
 str.toLocaleUpperCase()转大写
 str.toUpperCase();转大写
 str.trim();干掉字符串两端的空格
 str.fromCharCode(数字值,可以是多个参数),返回的是ASCII码对应的值

Array数组常见方法:

Array.isArray/instanceof/typeof/Object.prototype.toString.call:判断是否为数组    
map(函数): 遍历数组,返回回调返回值组成的新数组
forEach(函数): 遍历数组相当于for循环,无法break,可以用try/catch中throw new Error来停止
filter(函数): 过滤,返回的是数组中符合条件的元素,组成了一个新的数组
some(函数): 返回布尔类型,有一项符合条件返回true,则整体为true
every(函数): 返回布尔类型,有一项返回false,则整体为false
join(字符串): 通过指定连接符生成字符串
push / pop: 末尾推入和弹出,改变原数组, 返回推入之后新数组的长度和弹出的这个值
unshift / shift: 头部推入和弹出,改变原数组,返回推入之后新数组的长度和弹出的这个值
sort/ reverse: 排序(可能不稳定,请写MDN中的那个固定的代码)与反转,改变原数组
concat: 连接数组,不影响原数组, 浅拷贝
slice(start开始的索引, end结束的索引): 返回截断后的新数组,不改变原数组
splice(start开始的位置, number删除的个数, 替换的值): 一般用于删除数组中的元素,返回删除元素组成的数组,改变原数组
indexOf / lastIndexOf(要找的值value, 从某个位置开始的索引fromIndex): 从前往后/从后往前查找数组项,返回的是索引,没有则是-1
reduce(function(prev,cur,index,arr){...}, init);尽量提供一个初始值init,这样就可以从数组索引0开始执行,reduceRight() 用法和reduce一样,但是倒序排列。
reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。
prev 必需。累计器累计回调的返回值; 表示上一次调用回调时的返回值,或者初始值 init;
cur 必需。表示当前正在处理的数组元素;
index 可选。表示当前正在处理的数组元素的索引,若提供 init 值,则起始索引为- 0,否则起始索引为1;
arr 可选。表示原数组;
init 可选。表示初始值。

数组中splice和slice的区别

splice() 方法用于从数组中删除、替换或添加元素,并返回被删除的元素组成的数组,它会直接修改原数组。

  • 语法:array.splice(start, deleteCount, item1, item2, …)
  • 其中,start表示要修改的起始位置,deleteCount表示要删除的元素个数,item1、item2等表示要添加的元素。如果deleteCount为0,则表示只添加元素,不删除元素。

slice() 方法用于从数组中截取指定位置的元素,返回一个新的数组。

  • 语法是:array.slice(start, end),其中,start和end都是可选参数,表示选取的元素的起始位置和结束位置。如果不传入参数则默认选取整个数组。该方法返回的是一个新的数组,包含从start到end(不包括end)的元素。

Math常见方法:

Math.ceil(x)---向上取整 
Math.floor(x)---向下取整 
Math.Pi----圆周率的值 Math.Max(num1,num2,...)---一组数字中的最大值 
Math.Min(num1,num2,...)---一组数字中的最小值  
Math.abs(x)----绝对值 
Math.random()---随机数字(范围0~1)  
Math.sqrt(x)----开平方 
Math.pow(x,y)----一个数字的多少次幂   x^y

Date常见方法:

 var dt=new Date(); console.log(dt);   当前的时间---当前的服务器 
 var dt=new Date("2019-01-11");  //传入时间   
 dt.getFullYear();//年  
 dt.getMonth();//月---从0开始   
 dt.getDate();//日    
 dt.getHours();//小时  
 dt.getMinutes();//分钟   
 dt.getSeconds();//秒   
 dt.getDay();//星期---从  0  星期日  开始  
 dt.toDateString();//日期    
 dt.toLocaleDateString();//日期  
 dt.toTimeString();//时间   
 dt.toLocaleTimeString();//时间  
 dt.valueOf();//毫秒

Web Worke

JS是单线程的,有时候在处理数据量较大的业务时,会造成线程阻塞,就会影响用户使用体验,出现卡顿等情况。针对这个缺点,H5推出了web worker,使JS支持多线程。
Web Worker 是HTML5标准的一部分它允许一段JavaScript程序运行在主线程之外的另外一个线程中。主线程通常用于处理用户界面的交互和渲染,而 Web Worker 可以用于执行一些耗时的计算、网络请求、数据处理等任务,以提高应用的性能和响应速度。
它的主要特点是能进行并行计算,它允许在后台同时运行多个线程,这些线程可以并行执行任务。这使得可以同时处理多个耗时的操作,而不会阻塞用户界面。
应用场景

  • 大图片canvas转base64的时候
  • 对大量数据进行加密时
  • 读取解析一个较大的json文件
  • 在做一些插件时,比如代码的拼写检查,代码高亮,实时文本等
  • 处理音视频数据
  • 处理一些较大的数组集合

TS

JS和TS的区别

TS是JS的超集(TS是JS的升级版),新增了可选的静态类型和基于类的面向对象编程。 TS的目的是为了更安全高效的类型提示和检查

  • JS是动态类型 弱类型 TS是静态类型 强类型
  • TS的代码结构更加清晰,变量类型更加明确,有利于后期代码的维护;
  • TS支持代码静态检查,可以在代码执行前就完成代码的检查,减小了运行时异常的出现的几率

interface和type的区别

interface 可以声明一个接口,并且可以在其中定义函数的签名和可选属性。接口也可以用于描述类的实例部分和静态部分。
type 可以创建一个类型别名,可以表示任何有效的类型。类型别名还可以为复杂的联合类型、交叉类型等创建别名。

interface Person { name: string; age: number; }type Person = { name: string; age: number; }

相同点:

  • 都用于定义对象的类型
  • 都允许拓展

不同点:

  • type可以声明基本类型别名,联合类型,元组等类型;type语句中还可以使用typeof获取实例的类型进行赋值;
  • interface 可以通过 extends 关键字来扩展其他接口或类,实现接口的继承。而 type 可以使用交叉类型(&)来实现类型的扩展合并。
  • 如果要定义一个对象的结构,并且需要支持继承或实现,通常使用 interface。如果需要组合多个类型或者创建类型别名,以及其他高级类型操作,通常使用 type。

泛型

泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,使用时再去指定类型的一种特性。
可以把泛型理解为代表类型的参数。
使用泛型可以提高代码的可读性、可维护性和安全性,并增加代码的重用性,使代码更具灵活性和扩展性。

泛型的实现原理:
类型擦除:在编译过程中,TypeScript 编译器会将泛型类型擦除为其约束的最宽泛类型
类型推断:在使用泛型的地方,TypeScript 编译器会根据上下文推断出泛型的具体类型。如果无法推断,开发人员可以显式指定泛型的类型。
类型约束:通过使用泛型约束(Generic Constraints),可以限制泛型参数的类型范围。
类型替换:在编译过程中,TypeScript 编译器会将泛型类型替换为相应的具体类型。

操作系统

进程和线程以及它们的区别

  • 进程是对程序运行时的封装,是系统进行资源调度和分配的的基本单位(最小的资源管理单元),实现了操作系统的并发,应用程序的启动实例,进程拥有代码和打开的文件资源、数据资源、独立的内存空间
  • 线程是进程的子任务,是CPU调度和分配的基本单位(最小的执行单元),用于保证程序的实时性,实现进程内部的并发;
  • 一个程序至少有一个进程,一个进程至少有一个线程,线程依赖于进程而存在;
  • 进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存

协程

协程,又称微线程,纤程。协程是一种用户态的轻量级线程,协程的调度完全由用户控制(进程和线程都是由cpu 内核进行调度)。特点:

  • 协程特殊的程序控制
  • 一个线程可以拥有多个协程
  • 可以暂停执行(暂停的表达式称为暂停点)
  • 可以从挂起点恢复(保留其原始参数和局部变量)
  • 事件循环是异步编程的底层基石

并发和并行

并发:在操作系统中,某一时间段,几个程序在同一个CPU上运行,但在任意一个时间点上,只有一个程序在CPU上运行。
并行:当操作系统有多个CPU时,一个CPU处理A线程,另一个CPU处理B线程,两个线程互相不抢占CPU资源,可以同时进行,这种方式成为并行。
区别

  • 并发只是在宏观上给人感觉有多个程序在同时运行,但在实际的单CPU系统中,每一时刻只有一个程序在运行,微观上这些程序是分时交替执行。
  • 在多CPU系统中,将这些并发执行的程序分配到不同的CPU上处理,每个CPU用来处理一个程序,这样多个程序便可以实现同时执行。
  • 并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。它们最关键的点就是:是否是同时。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值