2022年JS面试总结

145 阅读6分钟

JS数据类型

JS数据类型分为两类: 一类是基本数据类型,也叫简单数据类型,包含7种类型,分别是Number 、String、Boolean、BigInt、Symbol、Null、Undefined。 另一类是引用数据类型也叫复杂数据类型,通常用Object代表,普通对象,数组,正则,日期,殊的基本包装类型(String、Number、Boolean),Math数学函数都属于Object。

数据分成两大类的本质区别: 基本数据类型和引用数据类型它们在内存中的存储方式不同。 基本数据类型是直接存储在栈中的简单数据段,占据空间小,属于被频繁使用的数据。 引用数据类型是存储在堆内存中,占据空间大。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址,当解释器寻找引用值时,会检索其在栈中的地址,取得地址后从堆中获得实体。

判断变量的类型

JavaScript有4种方法判断变量的类型,分别是typeof、instanceof、Object.prototype.toString.call()(对象原型链判断方法)、 constructor (用于引用数据类型)

1、typeof:常用于判断基本数据类型,对于引用数据类型除了function返回’function‘,其余全部返回’object'。

2、instanceof:主要用于区分引用数据类型,检测方法是检测的类型在当前实例的原型链上,用其检测出来的结果都是true,不太适合用于简单数据类型的检测,检测过程繁琐且对于简单数据类型中的undefined, null, symbol检测不出来。 instanceof的实现原理:验证当前类的原型prototype是否会出现在实例的原型链proto上,只要在它的原型链上,则结果都为true。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,找到返回true,未找到返回false。

3、constructor:用于检测引用数据类型,检测方法是获取实例的构造函数判断和某个类是否相同,如果相同就说明该数据是符合那个数据类型的,这种方法不会把原型链上的其他类也加入进来,避免了原型链的干扰。

4、Object.prototype.toString.call():适用于所有类型的判断检测,检测方法是Object.prototype.toString.call(数据) 返回的是该数据类型的字符串。原理:Object.prototype.toString 表示一个返回对象类型的字符串,call()方法可以改变this的指向,那么把Object.prototype.toString()方法指向不同的数据类型上面,返回不同的结果

这四种判断数据类型的方法中,各种数据类型都能检测且检测精准的就是Object.prototype.toString.call()这种方法。

判断数组的方式

1、Object.prototype.toString.call()做判断
2、通过原型链做判断
3、通过ES6的Array.isArray()做判断
4、通过instanceof做判断
6、通过Array.prototype.isPrototypeOf

数组去重都有哪些方法?

1、利用对象属性key排除重复项:遍历数组,每次判断对象中是否存在该属性,不存在就存储在新数组中,并且把数组元素作为key,设置一个值,存储在对象中,最后返回新数组。这个方法的优点是效率较高,缺点是占用了较多空间,使用的额外空间有一个查询对象和一个新的数组

2、利用Set类型数据无重复项:new 一个 Set,参数为需要去重的数组,Set 会自动删除重复的元素,再将 Set 转为数组返回。这个方法的优点是效率更高,代码简单,思路清晰,缺点是可能会有兼容性问题

3、filter+indexof 去重:这个方法和第一种方法类似,利用 Array 自带的 filter 方法,返回 arr.indexOf(num) 等于 index 的num。原理就是 indexOf 会返回最先找到的数字的索引,假设数组是 [1, 1],在对第二个1使用 indexOf 方法时,返回的是第一个1的索引0。这个方法的优点是可以在去重的时候插入对元素的操作,可拓展性强。

4、这个方法比较巧妙,从头遍历数组,如果元素在前面出现过,则将当前元素挪到最后面,继续遍历,直到遍历完所有元素,之后将那些被挪到后面的元素抛弃。这个方法因为是直接操作数组,占用内存较少。

5、reduce +includes去重:这个方法就是利用reduce遍历和传入一个空数组作为去重后的新数组,然后内部判断新数组中是否存在当前遍历的元素,不存在就插入到新数组中。这种方法时间消耗多,内存空间也有额外占用。

以上五个方法中,在数据低于10000条的时候没有明显的差别,高于10000条,第一种和第二种的时间消耗最少,后面三种时间消耗依次增加,由于第一种内存空间消耗比较多,且现在很多项目不再考虑低版本浏览器的兼容性问题,所以建议使用第二种去重方法,简洁方便。

               var arr = [1,1,3,3,2,2,4];
                //方法一
              for(let i=0;i<arr.length;i++){
                  for(let j=i+1;j<arr.length;j++){
                      if(arr[i]===arr[j]){
                          // 后面数据的若跟前一项数据相同,则重复,需要去除。
                          arr.splice(j,1);
                          j--;
                      }
                  }
              }
               //方法二
              let res = [...new Set(arr)];
               //方法三
              let res = arr.filter((item,index,arr)=>{
                // 从数组0位开始查,如果当前元素在原始数组中的第一个索引==当前索引值,说明它是第一次出现。
                return arr.indexOf(item,0)===index;
              })
               //方法四
              let res = arr.reduce((pre,cur)=>pre.includes(cur)?pre:[...pre,cur],[])

数组和伪数组的区别

伪数组它的类型不是Array,而是Object,而数组类型是Array。类数组可以使用的length属性查看长度,也可以使用[index]获取某个元素,但是不能使用数组的其他方法,也不能改变长度,遍历使用for in方法。

伪数组的常见场景:

  • 函数的参数arguments
  • 原生js获取DOM:document.querySelector('div') 等
  • jquery获取DOM:$(“div”)等

伪数组转换成真数组方法

  • Array.prototype.slice.call(伪数组)
  • [].slice.call(伪数组)
  • Array.from(伪数组) 转换后的数组长度由 length 属性决定。索引不连续时转换结果是连续的,会自动补位。

数组的遍历方法有哪些

image.png

new之后发生什么?

new 关键字会进行如下的操作:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this

new关键字后面的构造函数不能是箭头函数,如果new一个箭头函数第2、3步,箭头函数都是没有办法执行的。

this指向(普通函数、箭头函数)

this用于在对象内部的方法中使用对象内部的属性。但是由于JavaScript 的作用域机制的限制设计了this三种存在的场景,全局执行上下文和函数执行上下文和eval执行上下文

  • 在全局执行环境中无论是否在严格模式下,(在任何函数体外部)this 都指向全局对象。
  • 在函数执行上下文中访问this,函数的调用方式决定了 this 的值。

在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window,通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。

普通函数this指向:当函数被正常调用时,在严格模式下,this 值是 undefined,非严格模式下 this 指向的是全局对象 window;通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。new 关键字构建好了一个新对象,并且构造函数中的 this 其实就是新对象本身。嵌套函数中的 this 不会继承外层函数的 this 值。

箭头函数this指向:箭头函数并不会创建其自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数。
箭头函数因为没有this,所以也不能作为构造函数,但是需要继承函数外部this的时候,使用箭头函数比较方便

call, apply, bind 区别

call 和 apply 都是为了解决改变 this 的指向。作⽤都是相同的,只是传参的⽅式不同。

除了第⼀个参数外, call 可以接收⼀个参数列表, apply 只接受⼀个参数数组

//可以通过如下方式来调用:
func.call(this, arg1, arg2);
func.apply(this, [arg1, arg2])

其中 this 是你想指定的上下文,他可以是任何一个 JavaScript 对象(JavaScript 中一切皆对象),call 需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。  

let a = { value: 1 }
function getValue(name, age) {
   console.log(name)
   console.log(age)
   console.log(this.value)
}
getValue.call(a, 'yck', '24') 
getValue.apply(a, ['yck', '24'])
//这里都会按顺序输出值

. apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;
. apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;
. apply 、 call 、bind 三者都可以利用后续参数传参;
bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用

模拟实现 call apply

/* 可以从以下⼏点来考虑如何实现:
  不传⼊第⼀个参数,那么默认为 window
  改变了 this 指向,让新的对象可以执⾏该函数。
  那么思路是否可以变成给新的对象添加⼀个函数,然后在执⾏完以后删除?*/
Function.prototype.myCall = function (context) {
    var context = context || window
    // 给 context 添加⼀个属性
    // getValue.call(a, 'yck', '24') => a.fn = getValue
    context.fn = this
    // 将 context 后⾯的参数取出来
    var args = [...arguments].slice(1)
    // getValue.call(a, 'yck', '24') => a.fn('yck', '24')
    var result = context.fn(...args)
    // 删除 fn
    delete context.fn
    return result
}
// apply
Function.prototype.myApply = function (context) {
    var context = context || window
    context.fn = this
    var result
    // 需要判断是否存储第⼆个参数
    // 如果存在,就将第⼆个参数展开
    if (arguments[1]) {
       result = context.fn(...arguments[1])
    } else {
        result = context.fn()
     }
    delete context.fn
    return result
}
//bind 和其他两个⽅法作⽤也是⼀致的,只是该⽅法会返回⼀个函数。并且我们可以通过bind 实现柯⾥化。
Function.prototype.myBind = function (context) {
     if (typeof this !== 'function') {
          throw new TypeError('Error')
     }
     var _this = this
     var args = [...arguments].slice(1)
     // 返回⼀个函数
     return function F() {
        // 因为返回了⼀个函数,我们可以 new F(),所以需要判断
        if (this instanceof F) {
            return new _this(...args, ...arguments)
        }
        return _this.apply(context, args.concat(...arguments))
     }
}

对闭包的理解

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

闭包有两个常用的用途;

  • 第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
  • 另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

浅拷贝与深拷贝

参考:juejin.cn/post/684490…

  • 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象
  • 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
var a1 = {b: {c: {}};
​
var a2 = shallowClone(a1); // 浅拷贝方法
a2.b.c === a1.b.c // true 新旧对象还是共享同一块内存
​
var a3 = deepClone(a3); // 深拷贝方法
a3.b.c === a1.b.c // false 新对象跟原对象不共享内存

浅拷贝的实现方式

1.Object.assign()

2.函数库lodash的_.clone方法

3.展开运算符...

4.Array.prototype.concat()

5.Array.prototype.slice()

深拷贝的实现方式

1.JSON.parse(JSON.stringify())

2.函数库lodash的_.cloneDeep方法

3.手写递归方法

map 和 forEach 的区别

  • map有返回值,可以开辟新空间,return出来一个length和原数组一致的数组,即便数组元素是undefined或者是null。
  • forEach默认无返回值,返回结果为undefined,可以通过在函数体内部使用索引修改数组元素。

map的处理速度比forEach快,而且返回一个新的数组,方便链式调用其他数组新方法,比如filter、reduce

let arr = [1, 2, 3, 4, 5]; 
let arr2 = arr.map(value => value * value).filter(value => value > 10); // arr2 = [16, 25]

相同点:

  1. 都是循环遍历数组中的每一项
  2. 每次执行匿名函数都支持三个参数,参数分别为item(当前每一项),index(索引值),arr(原数组)
  3. 匿名函数中的this都是指向window
  4. 只能遍历数组

不同点:

  1. map()会分配内存空间存储新数组并返回,forEach()不会返回数据。
  2. forEach()允许callback更改原始数组的元素。map()返回新的数组。

map和Object的区别

image.png

Set、Map、WeakSet 和 WeakMap 的区别?

Set

  1. 成员不能重复;
  2. 只有键值,没有键名,有点类似数组;
  3. 可以遍历,方法有add、delete、has

WeakSet

  1. 成员都是对象(引用);
  2. 成员都是弱引用,随时可以消失(不计入垃圾回收机制)。可以用来保存 DOM 节点,不容易造成内存泄露;
  3. 不能遍历,方法有add、delete、has;

Map

  1. 本质上是键值对的集合,类似集合;
  2. 可以遍历,方法很多,可以跟各种数据格式转换;

WeakMap

  1. 只接收对象为键名(null 除外),不接受其他类型的值作为键名;
  2. 键名指向的对象,不计入垃圾回收机制;
  3. 不能遍历,方法同get、set、has、delete;

map和weakMap的区别

(1)Map map本质上就是键值对的集合,但是普通的Object中的键值对中的键只能是字符串。而ES6提供的Map数据结构类似于对象,但是它的键不限制范围,可以是任意类型,是一种更加完善的Hash结构。如果Map的键是一个原始数据类型,只要两个键严格相同,就视为是同一个键。

实际上Map是一个数组,它的每一个数据也都是一个数组,其形式如下:

const map = [
     ["name","张三"],
     ["age",18],
]

Map数据结构有以下操作方法:

  • sizemap.size 返回Map结构的成员总数。
  • set(key,value) :设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用)
  • get(key) :该方法读取key对应的键值,如果找不到key,返回undefined。
  • has(key) :该方法返回一个布尔值,表示某个键是否在当前Map对象中。
  • delete(key) :该方法删除某个键,返回true,如果删除失败,返回false。
  • clear() :map.clear()清除所有成员,没有返回值。

Map结构原生提供是三个遍历器生成函数和一个遍历方法

  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。
  • entries():返回所有成员的遍历器。
  • forEach():遍历Map的所有成员。
const map = new Map([
     ["foo",1],
     ["bar",2],
])
for(let key of map.keys()){
    console.log(key);  // foo bar
}
for(let value of map.values()){
     console.log(value); // 1 2
}
for(let items of map.entries()){
    console.log(items);  // ["foo",1]  ["bar",2]
}
map.forEach( (value,key,map) => {
     console.log(key,value); // foo 1    bar 2
})
复制代码

(2)WeakMap WeakMap 对象也是一组键值对的集合,其中的键是弱引用的。其键必须是对象,原始数据类型不能作为key值,而值可以是任意的。

该对象也有以下几种方法:

  • set(key,value) :设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用)
  • get(key) :该方法读取key对应的键值,如果找不到key,返回undefined。
  • has(key) :该方法返回一个布尔值,表示某个键是否在当前Map对象中。
  • delete(key) :该方法删除某个键,返回true,如果删除失败,返回false。

其clear()方法已经被弃用,所以可以通过创建一个空的WeakMap并替换原对象来实现清除。

WeakMap的设计目的在于,有时想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。一旦不再需要这两个对象,就必须手动删除这个引用,否则垃圾回收机制就不会释放对象占用的内存。

而WeakMap的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用

总结:

  • Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
  • WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。但是 WeakMap 只接受对象作为键名( null 除外),不接受其他类型的值作为键名。而且 WeakMap 的键名所指向的对象,不计入垃圾回收机制。

数组的方法

image.png

原型

构造函数是一种特殊的方法,主要用来在创建对象时初始化对象。每个构造函数都有prototype(原型)(箭头函数以及Function.prototype.bind()没有)属性, 这个prototype(原型)属性是一个指针,指向一个对象,这个对象的用途是包含特定类型的所有实例共享的属性和方法,即这个原型对象是用来给实例对象共享属性和方法的。每个实例对象的__proto__都指向这个构造函数/类的prototype属性。

原型链

函数的原型链对象constructor默认指向函数本身,原型对象除了有原型属性外,为了实现继承,还有一个原型链指针__proto__,该指针是指向上一层的原型对象,而上一层的原型对象的结构依然类似。因此可以利用__proto__一直指向Object的原型对象上,而Object原型对象用Object.prototype.__ proto__ = null表示原型链顶端。如此形成了js的原型链继承。

js继承的方法和优缺点

1、原型链继承:让一个构造函数的原型是另一个类型的实例,那么这个构造函数new出来的实例就具有该实例的属性,原型链继承的。

优点:写法方便简洁,容易理解。
缺点:在父类型构造函数中定义的引用类型值的实例属性,会在子类型原型上变成原型属性被所有子类型实例所共享。同时在创建子类型的实例时,不能向超类型的构造函数中传递参数。

2、借用构造函数继承:在子类型构造函数的内部调用父类型构造函数;使用 apply() 或 call() 方法将父对象的构造函数绑定在子对象上。

优点:解决了原型链实现继承的不能传参的问题和父类的原型共享的问题。
缺点:借用构造函数的缺点是方法都在构造函数中定义,因此无法实现函数复用。在父类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。

3、 组合继承:将原型链和借用构造函数的组合到一块。使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有自己的属性。
优点就是解决了原型链继承和借用构造函数继承造成的影响。
缺点是无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部

4、 原型式继承:在一个函数A内部创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。本质上,函数A是对传入的对象执行了一次浅复制。

ECMAScript 5通过增加Object.create()方法将原型式继承的概念规范化了。这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。在只有一个参数时,Object.create()与这里的函数A方法效果相同。

  • 优点是:不需要单独创建构造函数。
  • 缺点是:属性中包含的引用值始终会在相关对象间共享

5、寄生式继承:寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

  • 优点:写法简单,不需要单独创建构造函数。
  • 缺点:通过寄生式继承给对象添加函数会导致函数难以重用。

6、寄生组合式继承:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

  • 优点是:高效率只调用一次父构造函数,并且因此避免了在子原型上面创建不必要,多余的属性。与此同时,原型链还能保持不变;
  • 缺点是:代码复杂

节流防抖原理、区别以及应用

节流:事件触发后,规定时间内,事件处理函数不能再次被调用。也就是说在规定的时间内,函数只能被调用一次,且是最先被触发调用的那次。

防抖:多次触发事件,事件处理函数只能执行一次,并且是在触发操作结束时执行。也就是说,当一个事件被触发准备执行事件函数前,会等待一定的时间(自己去定义的),如果没有再次被触发,那么就执行,如果被触发了,那就本次作废,重新从新触发的时间开始计算,并再次等待 1 秒,直到能最终执行!

使用场景
节流:滚动加载更多、搜索框搜索联想功能、高频点击、表单重复提交……
防抖:搜索框搜索输入,并在输入完以后自动搜索、手机号,邮箱验证输入检测、窗口大小 resize 变化后,再重新渲染。按钮同时实现单击双击功能。

事件循环、同步和异步

JS是单线程的,为了防止一个函数执行时间过长阻塞后面的代码,所以会先将同步代码压入执行栈中,依次执行,将异步代码推入异步队列,异步队列又分为宏任务队列和微任务队列,因为宏任务队列的执行时间较长,所以微任务队列要优先于宏任务队列。
微任务队列的代表就是,Promise.thenMutationObserver
宏任务的话就是setImmediate setTimeout setInterval

JS运行的环境。一般为浏览器或者Node。 在浏览器环境中,有JS 引擎线程和渲染线程,且两个线程互斥。 Node环境中,只有JS 线程。 不同环境执行机制有差异,不同任务进入不同Event Queue队列。 当主程结束,先执行准备好微任务,然后再执行准备好的宏任务,一个轮询结束。

同步

  • 指在 主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务。
  • 也就是调用一旦开始,必须这个调用 返回结果(划重点——)才能继续往后执行。程序的执行顺序和任务排列顺序是一致的。

异步

  • 异步任务是指不进入主线程,而进入 任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程。
  • 每一个任务有一个或多个 回调函数。前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行。
  • 程序的执行顺序和任务的排列顺序是不一致的,异步的。
  • 我们常用的setTimeout和setInterval函数,Ajax都是异步操作。

ES6新特性

1. 变量和作用域

1.1 letconst、 块级作用域和变量声明

let声明的变量只在所在块中生效;

let声明的变量可以解决varfor循环结合使用产生的无法取得最新变量值的问题(以往都需要通过闭包来解决这个问题);

let声明的变量不存在变量提升(从undefined->ReferenceError,
其实也是一种暂时性死区
会造成变量暂时性死区(在声明let变量之前都不能用它)
也不允许重复声明;

const声明的变量行为与let类似,只是多了两点更强的约束:
1.声明时必须赋值;
2.声明的变量内存地址不可变,需要注意的是:对于用const声明基本类型,值就保存在内存地址之中,意味着变量不可重新赋值;
对于用const声明的对象,对象内容还是可以更改的,只是不能改变其指向。(冻结对象应该用Object.freeze())

1.2 解构赋值(按照一定的结构解析出来进行赋值)

解构赋值的使用场景:变量快捷赋值、提取数据、函数参数定义和默认值、遍历某结构

原生对象的方法扩展

2.1 String

加强了对unicode的支持、支持字符串遍历(后面有讲到实际上是部署了iterator接口)、repeat()等方法的支持、模板字符串

2.2 RegExp

构造函数第一个参数是正则表达式,指定第二个参数不再报错、u修饰符、y修饰符、s修饰符

2.3 Number

二进制和八进制新写法、新方法parseInt()、Number.EPSILON极小常量、安全整数、Math新方法

2.4 Function

函数参数默认值、rest参数、函数内部严格模式、函数的name属性、箭头函数

2.5 Array

扩展运算符...

2.6 ObjectSymbol

(1) Object对象

支持简写:同名属性K-V可以省略一个、函数声明可以省略function;支持属性名表达式、函数名表达式。(注意:以上2个——表达式和简写不能同时使用)。

对象的方法的name属性返回方法名,但有几个例外情况要小心。

新增了Object方法

Object.is()——用于解决==和===的部分兼容问题

Object.assign()——将src的所有可枚举对象属性复制到dest对象上(浅复制)

Object.setPrototypeOf()、Object.getPrototypeOf() (Object.__proto属性)

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

ES65种遍历对象属性的方法

for-in——自身和继承的可枚举属性(除SymbolObject.keys()——自身非继承的可枚举属性(除SymbolObject.getOwnPropertyNames()——自身所有属性键名(包括不可枚举、除SymbolObject.getOwnPropertySymbols()——自身的所有 Symbol 属性的键名

Reflect.ownKeys()——自身的所有键名

(2Symbol类型

ES5以前,对象属性都只能是字符串,容易造成重命名导致的冲突。Symbol提供了一种机制,可以保存 属性名是独一无二的。Symbol类型的使用注意:1)创建是调用函数,而不是new关键字 2Symbol类 型的属性不会被for-*、Object.keys()、Object.getPropertyNames()返回,可以用后面两种方法遍历。

数据结构Set和Map

Set是一种类似数组的数据结构,区别在于其存储的成员都是不重复的,由此带来了它的一个应用就是:去重。Set通过new关键字实例化,入参可以是数组or类数组的对象。

值得注意的是:在Set中,只能存储一个NaN,这说明在Set数据结构中,NaN等于NaN。

Set实例的方法:
操作方法add()、delete()、has()和clear();
遍历方法:keys()、values()、entries()和forEach();
扩展运算符...、数组方法map()、filter()方法也可以用于Set结构。由此它可以很方便的实现数组的交、并、差集。

WeakSet类似于Set,主要区别在于1.成员只能是对象类型;2.对象都是弱引用**(如果其他对象都不再引用该对象,垃圾回收机制会自动回收该对象所占的内存,不可预测何时会发生,故WeakSet不可被遍历)

JavaScript对象Object都是键值K-V对的集合,但K取值只能是字符串和Symbol,Map也是K-V的集合,然而其K可以取任意类型。如果需要键值对的集合,Map比Object更适合。Map通过new关键字实例化。

Map实例的方法:set()、get()、has()、delete()和clear();遍历方法同Set。

Map与其它数据结构的互相转换:Map <---> 数组| Map <---> 对象| Map <---> JSON。

WeakMap类似于Map,主要区别在于:
1.只接受对象作为键名;
2.键名所指向的对象不计入垃圾回收机制。

元编程相关Proxy和Reflect

4.1 Proxy

对目标对象加一层“拦截”(“代理”),外界对对象的访问、修改都必须先通过这层拦截层。因而它提供了 一个机制可以对外界的访问进行过滤和改写。

用法:var proxy = new Proxy(p1,p2); p1是要被代理的目标对象,p2是配置对象。

值得注意的是:Proxy不是对目标对象透明的代理——即使不做任何拦截的情况下无法保证代理对象与目 标对象行为的完全一致。(主要原因在于代理时,目标对象内部的this会指向代理对象)

4.2 ReflectProxy一样是ES6为语言层面的用于操作对象提供的新API,目前它所拥有的对象方法与Proxy对象一一对 应,引入目的:
1.Object对象上一些属于语言内部的方法放在Reflect上(目前都可以放)
2.修改Object对 象上某些方法的返回值,使得更加合理化(健壮)
3.Object对象的操作从命令式完全转化为函数式

异步编程Promise、Generator和Async

JavaScript的世界里,对于异步编程存在如下几种方案:
1.回调函数;
2.事件触发监听;
3.发布订阅者模式;
4.Promise。
首先介绍Promise,然后介绍ES6提供的生成器函数,async函数。

Promise来源于社区,代表一个对象,它代表异步操作未来的一个结果(承诺)。它总共有**三个状态**,pending\fulfilled\rejected。另外,它的状态**翻转路径只有两个**:pending->fulfilled or pending->rejected,一旦状态翻转,就不可变了。它支持链式调用,支持错误传递,支持以同步代码的方式写异步操作。

Promise是一个对象,创建此对象实例的方法如下(可以理解resolve和reject是已返回的承诺对象未来回调函数的占位)

Generator函数是ES6提供的异步编程解决方案。对于Generator函数,可以将它**理解为一个状态机,封装了多个内部状态;此外它还是一个遍历器生成函数**,这个函数可以遍历出状态机的所有状态。

函数特征:关键字function与函数名之间有,函数体内部yeild关键字。

生成器函数与普通函数的区别:函数调用后不执行,而是返回一个指针对象(遍历器对象)。调用对象的next()方法,执行一段yield逻辑。故函数的分段执行的,yield是暂停执行的标志,next()可以恢复执行。
yieldreturn的区别:yield有记忆功能,return没有;一个函数可以多次执行yeild,但只会return一次
async函数**是Generator函数的语法糖,它进行了**改进:1.自带执行器;2.返回值是Promise;

三家对比:使用Promise的异步代码存在大量自有API的调用,操作本身的语义夹杂其中,不是很清晰;Generator函数实现的异步代码语义比Promise清晰,但需要一个执行器;async函数的写法最简洁、符合语义,不需要执行器。

语言层面类、模块的支持

6.1 classES6 开始,JavaScript 提供了 class 关键字来定义类,尽管,这样的方案仍然是基于原型运行时系统的模拟,大部分功能ES5可以实现。

构造函数的prototype属性在 ES6 的“类”上面继续存在。事实上,类中所有方法都定义在类的prototype属性上面(因而也是不可枚举的)。

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。(默认构造函数);constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。

注意区别:类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层。

私有属性和方法如何实现?
1.命名上加以区别 
2.将私有方法移出模块,利用公有方法调用;
3.Symbol属性上(都不完美)

6.2 moduleES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJSAMD 两种。前者用于服务器,后者用于浏览器。ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJSAMD 模块,都只能在运行时确定。

编译时加载VS运行时加载——对象VS代码

模块命令:exportimport;一个文件即为一个模块,除非导入否则外部无法读取模块属性;

export支持:变量、函数和类

export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import命令也是如此。

输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

模块之间也可以继承。

JS中对象分类、及其它原生对象

image.png

Iterator

ES6之前在JS中只有Array和对象可以表示“集合”这种数据结构,
ES6中增加了:SetMap。由此,四种之间互相组合又可以定义新的数据结构。
这些新定义的数据结构如何访问呢?
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。
任何数据结构只要部署 Iterator 接口,就可以完成遍历操作。遍历器对象本质上是一个指针对象。

只要为某个数据结构部署了Iterator接口,则可以称此数据结构是可遍历的。iterator属性部署在Symbol上。
如下对象默认部署了Iterator结口:Array Set Map String等。
部署iterator结构的要点:
1)在Symbol.iterator上部署;
2)必须包含next()函数。默认调用iterator接口的场景:解构赋值、...扩展运算符、yeild 。for-of循环内部调用的即是调用数据机构内部的Symbol.iterator方法。

for-infor-of循环

for-in用于遍历对象属性,对象自身和继承的可枚举属性(不可遍历Symbol属性)

for-of循环是一种遍历所有数据机构的统一方法。实现原理是数据结构上部署的Symbol.iterator属性。

iframe有什么优点、缺点

优点:

  1. iframe能够原封不动的把嵌入的网页展现出来。
  2. 如果有多个网页引用iframe,那么你只需要修改iframe的内容,就可以实现调用的每一个页面内容的更改,方便快捷。
  3. 网页如果为了统一风格,头部和版本都是一样的,就可以写成一个页面,用iframe来嵌套,可以增加代码的可重用。
  4. 如果遇到加载缓慢的第三方内容如图标和广告,这些问题可以由iframe来解决。

缺点:

  1. iframe会阻塞主页面的onload事件;
  2. iframe和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载。会产生很多页面,不容易管理。
  3. iframe框架结构有时会让人感到迷惑,如果框架个数多的话,可能会出现上下、左右滚动条,会分散访问者的注意力,用户体验度差。
  4. 代码复杂,无法被一些搜索引擎索引到,这一点很关键,现在的搜索引擎爬虫还不能很好的处理iframe中的内容,所以使用iframe会不利于搜索引擎优化(SEO)。
  5. 很多的移动设备无法完全显示框架,设备兼容性差。
  6. iframe框架页面会增加服务器的http请求,对于大型网站是不可取的。

js 中有哪几种内存泄露的情况

  1. 意外的全局变量;
  2. 闭包;
  3. 未被清空的定时器;
  4. 未被销毁的事件监听;
  5. DOM 引用;

什么是 DOM 和 BOM?

  • DOM 指的是文档对象模型,它指的是把文档当做一个对象,这个对象主要定义了处理网页内容的方法和接口。
  • BOM 指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的法和接口。BOM的核心是 window,而 window 对象具有双重角色,它既是通过 js 访问浏览器窗口的一个接口,又是一个 Global(全局)对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。window 对象含有 location 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对象的子对象。

跨域

同源策略(Same-Origin Policy)最早由 Netscape 公司提出,是浏览器的一种安全策略。

同源: 协议、域名、端口号 必须完全相同。 违背同源策略就是跨域。

如何解决跨域

JSONP

  1. JSONP 是什么 :JSONP(JSON with Padding),是一个非官方的跨域解决方案,纯粹凭借程序员的聪明才智开发出来,只支持 get 请求。
  2. JSONP 怎么工作的?

在网页有一些标签天生具有跨域能力,比如:img link iframe script。 JSONP 就是利用 script 标签的跨域能力来发送请求的。

  1. JSONP 的使用
 // 1.动态的创建一个 script 标签 var script = document.createElement("script"); 
​
// 2.设置 script 的 src,设置回调函数 
​
script.src = "http://localhost:3000/testAJAX?callback=abc"; 
​
function abc(data) { 
​
      alert(data.name); 
​
}; 
​
//3.将 script 添加到 body 中 document.body.appendChild(script); 
​
//4.服务器中路由的处理 
​
router.get("/testAJAX" , function (req , res) { 
​
       console.log("收到请求"); 
​
       var callback = req.query.callback; 
​
       var obj = {
           name:"孙悟空", 
           age:18 
       }
       res.send(callback+"("+JSON.stringify(obj)+")");
});
  1. jQuery 中的 JSONP
//服务器配置
app.all('/jquery-jsonp-server',(request, response) => {
    // response.send('console.log("hello jsonp")');
    const data = {
        name:'尚硅谷',
        city: ['北京','上海','深圳']
    };
    //将数据转化为字符串
    let str = JSON.stringify(data);
    //接收 callback 参数
    let cb = request.query.callback;
    //返回结果
    response.end(`${cb}(${str})`);
});
//应用
$.getJSON('http://127.0.0.1:8000/jquery-jsonp-server?callback=?', function(data){ //必须加callback=?
      $('#result').html(`
          名称: ${data.name}<br>
          校区: ${data.city}
      `)
  });

CORS

  1. CORS 是什么?

CORS(Cross-Origin Resource Sharing),跨域资源共享。CORS 是官方的跨域解决方案,它的特点是不需要在客户端做任何特殊的操作,完全在服务器中进行处理,支持 get 和 post 请求。跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些 源站通过浏览器有权限访问哪些资源

  1. CORS 怎么工作的?

CORS 是通过设置一个响应头来告诉浏览器,该请求允许跨域,浏览器收到该响应以后就会对响应放行。

  1. CORS 的使用
//主要是服务器端的设置: 
​
router.get("/testAJAX" , function (req , res) {
​
       //通过 res 来设置响应头,来允许跨域请求 
​
       //res.set("Access-Control-Allow-Origin","http://127.0.0.1:3000"); 
​
       res.set("Access-Control-Allow-Origin","*"); 
​
       res.send("testAJAX 返回的响应"); 
​
}); 

nginx反向代理

实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。

使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。

实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。