1、作用域与作用域链

作用域本质上是程序存储和访问变量的规则,沿着作用域向上查找变量形成的链就是作用域链

JS中的作用域分为:全局作用域、函数作用域、块级作用域

JS是词法作用域(静态作用域)而不是动态作用域

词法作用域的特点:

作用域是在定义的位置决定的,而不是调用的位置

2、什么是闭包

闭包是指能够获取到另一个函数内部自由变量的函数。

闭包的表现形式:在函数内部返回函数、回调函数(函数作为参数)

闭包的优点:

①保存函数变量不受外界污染

②保存函数内的值,延迟执行

闭包的缺点:

①过度使用闭包会导致内存占用过多,所以要谨慎使用闭包

闭包的应用:防抖、节流、柯里化、封装模拟私有变量

栗子1:
function foo(a,b){
  console.log(b);
  return {
    foo:function(c){
      return foo(c,a);
    }
  }
}
栗子2:
var func1=foo(0);
func1.foo(1);
func1.foo(2);
func1.foo(3);
var func2=foo(0).foo(1).foo(2).foo(3);
var func3=foo(0).foo(1);
func3.foo(2);
func3.foo(3);

//防抖(在一定时间内触发多次,只会以最后一次的时间为准,使用计时器)
function debunce (fn, time=500) {
    let timeout = null;
    return function(...args){
        if(timeout) clearTimeout(timeout);
            timeout = setTimeout(() => {
                fn.call(this, ...args);
            }, time);
        }
    }
}

//节流(在一定时间内触发多次,只会以第一次为准,用标识符)
function throttle(fn, time=500) {
    let flag = true;
    return function(...args){
        if(!flag) return;
        flag=false;
        setTimeout(() => {
            fn.call(this, ...args);
            flag=true;
        }, time);
    }
}
function sayHi(e) {
    console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi));
复制代码

柯里化

柯里化是把接受 n 个参数的 1 个函数改造为只接受 1个参数的 n 个互相嵌套的函数的过程。也就是 fn (a, b, c)fn(a,b,c) 会变成 fn (a)(b)(c)fn(a)(b)(c);目的就是为了 “记住” 函数的一部分参数;

偏函数

固定你函数的某一个或几个参数,然后返回一个新的函数(这个函数用于接收剩下的参数)

3、变量提升和暂时性死区

函数声明的优先级高于变量声明,函数声明会被提升到代码块的最前面

变量提升的本质是js在编译阶段会找到所有的变量声明,并提前让声明生效;而暂时性死区的本质是js编译时感知到了变量的声明,但是是不可访问状态,所以声明之前使用会报错。

(1)变量提升:用var声明的变量会存在变量提升(声明会被提升,赋值并不会)

栗子0:
var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); 

栗子1:
var a = 1;
function test(){
    a = 2;
    return function(){
        console.log(a);
    }
    var a = 3;
}
test()();

栗子2:
var a = 0;
if(true) {
    a = 1;
    function a() {};
    a = 21;
    console.log('里面' + a);
}
console.log('外面' + a);

栗子3:
test();
console.log(test);
var test = '我是变量';
console.log(test);
var test = function (params) {
  console.log('我是函数表达式');
}
console.log(test);
function test() {
  console.log('我是函数');
}
console.log(test);
test();
复制代码

(2)暂时性死区:用let、const生命的变量存在暂时性死区

var me = 'icon';

{
	me = 'lee';
	let me;
}
//会报错
复制代码

4、哪些情况可能会导致内存泄漏

1. 意外的全局变量;

2. 闭包(老式浏览器闭包会造成内存泄漏,现在正确使用并不会造成内存泄漏);

3. 未被清空的定时器;

4. 未被销毁的事件监听;

5. 未清除的DOM 引用;

5、js的内存管理机制

基本类型存在栈中,引用类型存在堆中,栈中存的是引用类型的地址;

①引用计数法(已废弃)

②标记清除法

在标记清除算法中,一个变量是否被需要的判断标准,是它是否可抵达

这个算法有两个阶段,分别是标记阶段和清除阶段:

  • 标记阶段:垃圾收集器会先找到根对象,在浏览器里,根对象是 Window;在 Node 里根对象是 Global。从根对象出发,垃圾收集器会扫描所有可以通过根对象触及的变量,这些对象会被标记为 “可抵达”。
  • 清除阶段: 没有被标记为 “可抵达” 的变量,就会被认为是不需要的变量,这波变量会被清除

6、this的指向问题

(1)箭头函数中的this指向定义时的对象;

普通函数的this指向调用它的那个对象;

(2)非严格模式、非箭头函数,以下3种情况里的this一定指向window

  • 立即执行函数
  • setTimeout
  • setInterval

(3)严格模式下全局作用域下的this指向window,函数体内或代码块内的this为undefined

栗子1:
'use stric'
console.log(this);
function showMine(){
    console.log(this);
}

输出:
window
undefined

栗子2:
'use strict' 
var name = 'BigBear' 
var me = { 
    name: 'yeyong', 
    hello: function() { // 全局作用域下实现的延时函数 
        setTimeout(function() { 
            console.log(`你好,我是${this.name}`) 
         })
     }
} 
me.hello();
输出: 你好,我是BigBear
复制代码

(4)箭头函数的this始终指向定义时所在的对象

var a = 1
var obj = {
  a: 2,
  func2: () => {
    console.log(this.a)
  },
  
  func3: function() {
    console.log(this.a)
  }
}

// func1
var func1  = () => {
  console.log(this.a)
}

// func2
var func2 = obj.func2
// func3
var func3 = obj.func3

func1()
func2()
func3()
obj.func2()
obj.func3()
复制代码

7、改变this的指向的方法call/apply/bind

Function.prototype.call = function(context, ...args){
    let fn = Symbol();
    //这里的this即调用call的函数
    context.fn = this;
    //现在这个函数是被context调用的,所以这个函数内部的this指向了context
    context.fn(...args);
    delete context.fn;
}

Function.prototype.apply = function(context, args){
    let fn = Symbol();
    //这里的this即调用call的函数
    context.fn = this;
    //现在这个函数是被context调用的,所以这个函数内部的this指向了context
    context.fn(...args);
    delete context.fn;
}

Function.prototype.bind= function(context){
    return function (..args){
        call(context, ...args);
    }
}
复制代码

8、call 和 apply 的区别是什么,哪个性能更好一些

作用是一样的,区别在于传入参数的不同, 第一个参数都是指定函数体内 this 的指向,

  • 第二个参数开始不同,apply 是传入带下标的集合,数组或者类数组, apply 把它传给函数作为参数,call 从第二个开始传入的参数是不固定的, 都会传给函数作为参数;
  • call 比 apply 的性能要好,call 传入参数的格式正式内部所需要的格式;

8、箭头函数与普通函数(function)的区别 是什么?构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?为什么?

箭头函数是普通函数的简写,可以更优雅的定义一个函数,和普通函数相比,有 以下几点差异:

  • 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象;
  • 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以 用 rest 参数代替(...args收集参数);
  • 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数;
  • 不可以使用 new 命令,因为: A.没有自己的 this,无法调用 call、apply; B.没有 prototype 属性 ,而 new 命令 在执 行时 需要将构造函数的 prototype 赋值给新的对象的 __proto__。

9、new原理

new发生了什么:

  • 创建新对象(以构造函数的原型为原型)

  • 将构造函数的作用域赋值给新对象(因此this就指向了这个新对象)

  • 执行构造函数中的代码(为这个对象添加新属性)

  • 返回新对象

    //手写new function new (fn, ...args){ let object = Object.create(fn.prototype); let result = fn.call(object, ...args); return result instanceof Object? result : object; } //Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto_。 //Instanceof的判断队则是:沿着A的__proto__这条线来找,同时沿着B的prototype这条线来找, //如果两条线能找到同一个引用,即同一个对象,那么就返回true。如果找到终点还未重合,则返回false

10、原型与原型链

原型:每个函数都有一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constuctor(构造函数)属性,这个属性指向构造函数(或函数)自身。

每个对象都有一个__proto__,它指向创建这个对象的函数的prototype,即fn.__proto__ === Fn.prototype(由Object.create(null)创建的对象除外)

function Foo(){}
var f1 = new Foo();
f1 instanceof Foo;
f1 instanceof Object;

//Object是由Function创建的
Object instanceof Function
Function instanceof Object
Function instanceof Function
//都是true
复制代码

考题:
var A = function() {};
A.prototype.n = 1;
var b = new A();
A.prototype = {
  n: 2,
  m: 3
}
var c = new A();

console.log(b.n);
console.log(b.m);

console.log(c.n);
console.log(c.m);
//输出: 1 undefined 2 3

考题2:自有属性和原型继承属性
function A() {
    this.name = 'a'
    this.color = ['green', 'yellow']
 }
 function B() {}
 B.prototype = new A()
 var b1 = new B()
 var b2 = new B()
 
 b1.name = 'change'
 b1.color.push('black')

console.log(b2.name) // 'a'
console.log(b2.color) // ["green", "yellow", "black"]

考题3: typeof null === 'object',那null instanceof Object?

//考察变量提升优先级、作用域、this指向、new的执行顺序
function Foo() {    
    getName = function () { alert (1); };
    return this;
}
Foo.getName = function () { 
    alert (2);
};
Foo.prototype.getName = function () { 
    alert (3);
};
var getName = function () { 
    alert (4);
};
function getName() { 
    alert (5);
}
 //请写出以下输出结果:
 Foo.getName();
 getName();
 Foo().getName(); //这里的this指向window
 getName();
 new Foo.getName();   //同new (Foo.getName)();
 new Foo().getName();  //同(new Foo()).getName()
 new new Foo().getName();  //同new ((new Foo()).getName)();

// 2 4 1 1 2 3 3
new Foo().getName().getName(); //3 1
复制代码

new(带参数列表)

n/a

new … ( … )

new(无参数列表)

从右到左

new …

function Foo(){              
    Foo.a = function (){                
        console.log(1);              
    }              
    this.a = function(){                
        console.log(2)              
    }            
}            
Foo.prototype.a = function(){              
    console.log(3);            
}            
Foo.a = function(){              
    console.log(4);            
}            
Foo.a();        
let obj = new Foo();   
obj.a();            
Foo.a();
//4,2,1
复制代码

类数组转换成数组的方法有哪些

Array.from({ length: 3 });

[...arrLike] 扩展运算符,但是扩展运算符只能作用于 iterable 对象,即拥有 Symbol(Symbol.iterator) 属性的值,可以被for...of遍历;

Array.prototype.slice.call(arrayLike);

//类似的有
Array.apply(null, arrayLike)
Array.prototype.concat.apply([], arrayLike)


//以下几种方法需要考虑稀疏数组的转化
Array.prototype.filter.call(divs, x => 1)
Array.prototype.map.call(arrayLike, x => x)
Array.prototype.filter.call(arrayLike, x => 1)//一切将类数组做为 this 的方法将都返回稀疏数组,
//而将类数组做为 参数arguments 的方法将都返回密集数组
复制代码

去重的方法

//利用ES6的API
function unique(array) {
    return [...new Set(array)];
}
//利用indexOf
const a = [1,1,2,2,3,4,4,3,5]
function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    return arr.filter((item, index) => {
        return arr.indexOf(item) === index;
    });
}
//利用object的key值不能重复
function distinct(array) {
    var obj = {};
    return array.filter(function(item, index, array){
        return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
    })
}
//实现对象数组去重
var resources = [
            {id: 1, name: "张三", age: "18" },
            {id: 2, name: "张三", age: "19" },
            {id: 1, name: "张三", age: "20" },
            {id: 3, name: "李四", age: "19" },
            {id: 3, name: "王五", age: "20" },
            {id: 2, name: "赵六", age: "21" }
        ];
//filter实现
function unique(arr,uniId){
    let map = new Map();
    return arr.filter(item => {
       return !map.has(item[uniId]) && map.set(item[uniId], true);
    });
}
//reduce实现
function unique1(arr, uniId){
    let map = new Map();
    return arr.reduce((total, item)=>{
        map.has(item[uniId])? '': map.set(item[uniId], true) && total.push(item);
    return total;
    },[])
}
    
复制代码

数组扁平化的方法

1.for循环递归
function flatten(arr){
  var result = [];
  for(let i = 0; i< arr.length; i++){
    if(Array.isArray(arr[i])){
      result = result.concat(flatten(arr));
    }else{
      result = arr[i];
    }
  }
  return result;
}
2.toString()
const flatten = arr => arr.toString().split(',').map((item) => +item);
3.reduce
function flatten(arr) {
  return arr.reduce((pre, item)=>{
    return pre.concat(Array.isArray(item)? flatten(item) : item)
  }, []);
}
4.迭代,非递归
function flatten(arr){
  while(arr.some(item => Array.isArray(item))){
    arr = [].concat(...arr)
  }
  return arr;
}
5.可传入数字进行扁平化
let arr = [1,2,3,[4,5,[6,7],8],9]
Array.prototype.myFlat = function(num){
    num--;
    return num>=0? this.reduce((pre, item) => {
        return pre.concat(Array.isArray(item)? item.myFlat(num) : item);
    },[]) : this;
}
arr.myFlat(1);
复制代码

合并两个数组的方法

let a = [1,2]
let b = [3,4]

let c = [...a,...b]
let c = a.concat(b)
let c = a.push.apply(a,b)
for(let i of b){
    a.push(i)
}
复制代码

['1', '2', '3'].map(parseInt) what & why ?

['1', '2', '3'].map(parseInt) 的输出结果为 [1, NaN, NaN]。

因为 parseInt(string, radix) 将一个字符串 string 转换为 radix 进制的整数, radix 为介于 2-36 之间的数。

在数组的 map 方法的回调函数中会传入 item(遍历项) 和 index(遍历下标) 作为前两个参数,所以这里的 parseInt 执行了对应的三次分别是

parseInt(1, 0)

parseInt(2, 1)

parseInt(3, 2)

对应的执行结果分别为 1、NaN、NaN。

如果 radix 是 undefined、0或未指定的,JavaScript会假定以下情况: 

 如果输入的 string以 "0x"或 "0x"(一个0,后面是小写或大写的X)开头,那么radix被假定为16,字符串的其余部分被当做十六进制数去解析;

 如果输入的 string以 "0"(0)开头, radix被假定为8(八进制)或10(十进制)。具体选择哪一个radix取决于实现。ECMAScript 5 澄清了应该使用 10 (十进制),但不是所有的浏览器都支持。

因此,在使用 parseInt 时,一定要指定一个 radix。 如果输入的 string 以任何其他值开头, radix 是 10 (十进制)。 如果第一个字符不能转换为数字,parseInt会返回 NaN。

介绍下 Set、Map、WeakSet 和WeakMap 的区别?

Set

1).成员不能重复;

2).只有键值,没有键名,有点类似数组;

3).可以遍历,方法有 add、delete、has、clear

WeakSet

1).成员都是对象(引用);

WeakSet可以接受一个数组或类似数组的对象作为参数。

var a = [[1,2], [3,4]]; //数组a的成员必须是引用类型
var ws = new WeakSet(a);
复制代码

2).成员都是弱引用,随时可以消失(不计入垃圾回收机制)。可以用来保存DOM 节点,不会造成内存泄露;

3).不能遍历,没有size属性,不能清空clear(),方法有 add、delete、has;

Map

1).本质上是键值对的集合,类似集合;

2).可以遍历,方法很多,可以跟各种数据格式转换;

WeakMap

1).只接收对象为键名(null 除外),不接受其他类型的值作为键名;

2).键名指向的对象,不计入垃圾回收机制;WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失;

3).不能遍历,没有size属性,不能清空clear(),方法有 get、set、has、delete;

==》

Set结构的实例有以下属性。

  • Set.prototype.constructor:构造函数,默认就是Set函数。
  • Set.prototype.size:返回Set实例的成员总数。

Set实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。

  • add(value):添加某个值,返回Set结构本身。
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • has(value):返回一个布尔值,表示该值是否为Set的成员。
  • clear():清除所有成员,没有返回值

Set结构的实例有四个遍历方法,可以用于遍历成员。

  • keys():返回键名的遍历器
  • values():返回键值的遍历器
  • entries():返回键值对的遍历器
  • forEach():使用回调函数遍历每个成员

Map操作方法:

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

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

  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。
  • entries():返回所有成员的遍历器。
  • forEach():遍历Map的所有成员。

需要特别注意的是,Map的遍历顺序就是插入顺序。

WeakSet应用:

const foos = new WeakSet()
class Foo {
  constructor() {
    foos.add(this)
  }
  method () {
    if (!foos.has(this)) {
      throw new TypeError('Foo.prototype.method 只能在Foo的实例上调用!');
    }
  }
}
复制代码

上面代码保证了Foo的实例方法,只能在Foo的实例上调用。这里使用WeakSet的好处是,foos对实例的引用,不会被计入内存回收机制,所以删除实例的时候,不用考虑foos,也不会出现内存泄漏。

WeakMap应用:

WeakMap的设计目的在于,键名是对象的弱引用(垃圾回收机制不将该引用考虑在内),所以其所对应的对象可能会被自动回收。当对象被回收后,WeakMap自动移除对应的键值对。典型应用是,一个对应DOM元素的WeakMap结构,当某个DOM元素被清除,其所对应的WeakMap记录就会自动被移除。基本上,WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏。

下面是WeakMap结构的一个例子,可以看到用法上与Map几乎一样。

var wm = new WeakMap();
var element = document.querySelector(".element");

wm.set(element, "Original");
wm.get(element) // "Original"

element.parentNode.removeChild(element);
element = null;
wm.get(element) // undefined
复制代码

上面代码中,变量wm是一个WeakMap实例,我们将一个DOM节点element作为键名,然后销毁这个节点,element对应的键就自动消失了,再引用这个键名就返回undefined。

ES5/ES6 的继承除了写法以外还有什么区别?

ES5 的继承,实质是先创造子类的实例对象 this ,然后再将父类的方法添加到 this 上面( Parent.apply(this) )。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this 。

class A {}
class B extends A {
  constructor() {
    super();
  }
}
//super() 在这里相当于 A.prototype.constructor.call(this) 
复制代码

juejin.cn/post/684490…

setTimeout、Promise、Async/Await 的区别

setTimeout:setTimeout 的回调函数放到宏任务队列里,等到执行栈清空以后执行;

Promise

Promise 本身是同步的立即执行函数,有两个参数resolve和reject分别代表执行成功时的方法,和执行失败时的方法;遇到resolve或reject时,不会立即执行,会被放入微任务队列;

错误捕获用.catch(err)

async/await

async/await是genenrator的语法糖,在generator的基础上加了自执行函数;

async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体;

正常情况下, await 命令后面是一个Promise对象(当为thenable对象时,会被转换成Promise对象),返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值;

错误捕获用try...catch...

详情见异步专题:

隐式转换规则

  • 数组/对象转换成number的过程:先调用valueOf获取原始值,如果该原始值不是number类型,会继续调用toString转换成string然后在用Number转换成number;

  • 有八种类型转换成布尔值时返回false,其他都是true :

0,-0,null,undefined,false,''(空字符串),NaN,document.all()

Boolean([]),Boolean({})都是为true

Number([])为0,Number({})为NaN(+[]为0,+{}为NaN)

引用类型不相等 [] != [] {}!={}

  • 遇到关系运算符转换成数字Number() 例+、== 、>= 、!=...遇到逻辑运算符&&、||、!、条件运算符?转换成布尔值Boolean()

下面代码中 a 在什么情况下会打印 1?

 var a = ?;
if(a == 1 && a == 2 && a == 3){ console.log(1);}

解答: var a = { value: 0, valueOf() { return ++this.value; }};