陕西队西北狼

ES6 基础知识

ES6 面试题

引言

  - ECMAScript5,即 ES5 ,是ECMAScript 的第五次修订,于2009年完成标准化
  - ECMAScript6,即ES6,是ECMAScript的第六次修订,于2015年完成,也称ES2015

一、let, var, const 三者的比较

1、var 会导致的问题:

  • var 命令会导致变量提升现象,即变量可以在声明之前使用,值为undefined,变量会被提升到作用域的顶端。

        var a = 1;
        function test1(){
            console.log(a);//undefined ,因为这里存在变量提升,这里 a 没有先声明就使用了,会将 a 的声明提到顶部
            var a = 2;
            console.log(a);// 重新声明了 a 又给赋值,所以这里为 2
        }
    
        test1();
    
        //变量提升,如上面代码等价于
        var a = 1;
        function test2(){
            var a;//变量提升
            console.log(a);
            var a = 2;
            console.log(a);
        }
    
    
  • 内层变量可能覆盖外层变量

    如上述变量提升案例,实际上内层由于先使用 a 再定义 a,导致变量提升,那么内部的变量就把外部的变量a 就给污染了。
    
  • 用来计数的循环变量泄露为全局变量

      var s = [1, 2, 3, 4, 5];
      for (var i = 0; i < s.length; i++) {
        console.log(s[i]);
      }
    
      console.log(i) //5
    
      //上述代码,由于使用 var 声明变量,导致 i 泄露为全局变量。
    

2、let

  • 声明的全局变量不会挂在在顶层对象下(即let 声明的全局变量不会挂在window 或 global下面)

      //考察 let
      let a = 1;
      var b = 2;
      console.log(window.a) // undefiend, 因为 let 声明的全局变量不会挂在在全局对象下
      console.log(window.b)// 2 ,使用 var 声明的全局变量,会挂在全局对象下
    
  • 所有的变量一定要在声明后使用,否则会报错。

      console.log(a) //Uncaught ReferenceError: a is not defined
      let a = 1;
    
  • 暂时性死区,只要块级作用域内存在 let 命令,它所声明的变量就“绑定”( binding )这个区域,不再受外部的影响,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。

3、const

  • 同 let一样,声明的全局变量不会挂在顶层对象下面
  • const 声明后必须马上赋值,否则会报错
  • const 作为简单类型,一旦声明就不能更改,复杂类型(对象、数组等)指针的地址不能修改,内部数据可更改。
      const obj1 = { a: 1, b: 2 }
      obj1 = {};//报错,Uncaught SyntaxError: Identifier 'obj1' has already been declared
      obj1.b = 3;// 不报错,因为对象里面的值是可以改的
    
  • const 声明也不存在提升现象,同样存在暂时性死区,只能在声明后使用。

二、箭头函数

1、箭头函数this的指向问题,箭头函数与普通函数的区别,你说箭头函数没有自己的this,那么 (()=>{}).bind(this) 可以吗?

  • 箭头函数提供了一种更简洁的函数书写方式,不可以作为构造函数,也就是不能使用new命令,否则会报错

    var f = v => v
    // 等价于
    var f = function (a) {
        return a;
    }
    f(1) //1
    
  • 当箭头函数要返回对象的时候,为了区分与代码块,要用 () 将对象包裹起来

    //报错,Uncaught SyntaxError: Unexpected token ':'
    var f = (id, name) => { id: id, name:name }
    
    // 不报错,返回对象时,要使用 () 括起来,和 {} 做区分
    var f = (id, name) => ({ id: id, name: name })
    

2、箭头函数没有

  • 1、没有this
    箭头函数没有this,所以需要通过查找作用域链来确定this的值。这就意味着如果箭头函数被非箭头函数包含,this绑定的就是最近一层非箭头函数的this。
    因为箭头函数没有this,所以也不能用 call()、apply()、bind() 这些方法改变this的指向。

      var value = 1;
      var result = (() => this.value).bind({value: 2})();
      console.log(result); // 1,上述方法将 this绑定到新对象上,不会改变结果
    
  • 2、没有arguments

    箭头函数没有自己的arguments对象,因为箭头函数可以访问外围函数的,通过命名参数或者 rest 参数的形式访问参数。

  • 3、没有new关键字调用

    箭头函数不能被用作构造函数,如果通过new方式调用,会报错

    var Foo = () => {};
    var foo = new Foo(); // TypeError: Foo is not a constructor
    
  • 4、没有 new.target

    因为不能使用 new调用,所以也没有 new.target

  • 5、没有原型

    由于不能使用 new 调用箭头函数,所以也没有构造原型的需求,于是箭头函数不存在 prototype 这个属性

  • 6、没有super

    连原型都没有,自然也不能通过 super来访问原型的属性。

三、对象属性缩写

  • 1、对象的方法缩写

      //1、对象的方法缩写
      const o = { method() { return "hello"; } }
      //等同于
      const o = { method: function () { return "hello"; } }
    
      let birth = "1999/09/08";
      const Person = {
          name: "张三",
          birth,//等同于 birth:birth
          //等同于 hello:function (){ ...}
          hello() {
              console.log('我的名字是', this.name);
          }
      }
    
  • 2、对象的属性缩写

      const a = 'a';
      const b = { a };
      //等同于
      const b = { a: a };
    
      function f(x, y) { return { x, y } };
      //等同于
      function f(x, y) { return { x: x, y: y } }
    

四、Object.Keys()、Object.Values()、Object.Assign() 方法

1、Object.Keys()

参数:要求返回其没见自身属性的对象。
返回值:一个表示给定对象的所有可枚举属性的字符串数组:

  • 处理对象:返回可枚举的属性数组。

      let person = {name:"张三", age:25, address:"深圳", getName:function(){}}
      Object.keys(person) // ["name", "age", "address", "getName"]
    
  • 处理数组,返回索引值数组

      let str = "saasd字符串"
      Object.keys(str) // ["0", "1", "2", "3", "4", "5", "6", "7"]// 返回的是一个索引值数组
    
  • 常用技巧:

      let person = {name:"张三",age:25,address:"深圳",getName:function(){}}
      Object.keys(person).map((key)=>{
        person[key] // 获取到属性对应的值,做一些处理
      })
    

2、Object.values()Object.keys() 刚好相反,它是把一个对象的值转换为数组。

  let person = {name:"张三", age:25, address:"深圳", getName:function(){}}
  Object.keys(person).map((key)=>{
    person[key] // 获取到属性对应的值,做一些处理
  })

3、Object.Assign() 方法,用于合并对象、或者复制对象

  //Object.Assign(),可以用来合并对象,或者复制对象,简写可以使用操作运算符...
  let json = { a: 3, b: 4 };
  let json2 = { ...json } // 这种写法等价于 Object.Assign({},json)

  //注意合并后的对象和之前的对象不相等
  console.log(json2 == json) //false
  console.log(Object.assign({},json) == json)//false
  console.log(Object.assign(json) === json)// true,因为并没有合并成新对象

  //相同的键,后者会覆盖前者
  let json3 = { a: 4, c: 5 }
  let json4 = Object.assign(json, json3); //json4 返回 {a:4,b:4,c:5}

五、import /export/ ,require/export 区别

提问: ES6 中 import 和 export 和 CommonJS 中的 require/exports 中的区别是什么?

  • 1、CommonJS模块输出的是一个值的拷贝,一旦输出一个值,模块内部的变化就影响不到这个值

    ES6模块输出的是值的引用,JS引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。
    等到脚本真正执行时,在根据引用到被加载的那个模块里面去取值。
    ES6模块是动态引用,并且不会缓存运行结果,而是动态的去被加载的模块取值,模块里面的变量绑定其所在的模块。
    
  • 2、CommonJS 模块是运行时加载,ES6 模块是编译时输出接口(即ES6 可以在编译时就完成模块加载,效率要比CommonJS模块的加载方式高)

  • 3、ES6 模块是通用的,同一个模块不用修改,就可以用在浏览器

  • 4、require / exports 是 CommonJS 在 Node 中实现的,import / export 是 ES6 的模块,对ES6 只要使用 babel 转换就可以了。

  • 5、ES6 模块的设计思想,是尽量静态化,使得编译时就能确定模块的依赖关系以及输入和输出的变量,export命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能。

六、解构赋值

注意: 解构的核心思想,是左右两边,结构要保持一致

//1、初始化赋值,利用数组解构
let [a, b, c] = [1, 2, 3]

//2、对象解构

let { name, age, arr, f } = {
  name: "jerry",
  age: 32,
  arr: [{ name: 'liu' }, { name: 'zhao' }],
  f: function () { console.log("hello world") }
}

//3、数组右侧缺省解构
let [a, b, c] = ['a', 'b']// c 会结构不到,为undefined

//4、数组解构,有默认参数
let [a, b, c = 'no-data'] = ['a', 'b']// 解构不到的,会按默认值填充
console.log(a, b, c)//a b no-data

//5、a,b 交换
let a = 1;
let b = 2;
let [a, b] = [b, a]// 使用解构,快速交换两个变量的值

//4、解构传入undefined 等,不会被解构为具体值
function fn({ a, b = '默认值' }) {
  console.log(a)//1
  console.log(b)//默认值
}

fn({ a: 1, b: undefined }) // undefined 不会被解构出来
fn({ a: 1, b: null }) // null 会被解构出来
fn({ a: 1, b: '' }) //字符串空会被解构出来

七、 Map 和 Set

1、Set 类似数组,但里面不能有重复

  • Set 对象允许你存储任何类型的唯一值,无论原始数据是值还是对象引用
  //1、数组去重
  let arr = ['a','b','a'];
  let newArr = new Set(arr);

  //1、Set 用法:
  let setArr = new Set(['a','b']);
  setArr.add('a');//往 Set 里添加一项
  setArr.delete('b'); //删除Set 中的一项
  setArr.has('a');// true
  setArr.size// 1, 返回个数 ,注意这个是个属性,不是一个函数
  setArr.clear();//清空Set
  • Set 中的特殊值:(Set 对象存储的值总是唯一的,所以要判断两个值是否恒等,有几个值需要特殊对待)
- 1) +0 与 -0 在存储判断唯一性的时候是恒等的,所以不重复。
- 2) undefined 与 undefined 是恒等的,所以不重复。
- 3) NaN 与 NaN 是不恒等的,但是Set 中只能存一个,所以不重复 

2、Map 是ES6 中新增的数据结构,Map 类似于对象,但普通对象的key 必须是字符串或数字,而Map 的key 可以是任意数据类型

  • Map 常规操作

    const map = new Map();
    const obj = { p: "hello world" };
    
    map.set(obj, 'OK') // 设置一个值,key 可以使任意类型
    map.get(obj)// OK
    map.has(obj) //true
    map.delete(obj)//true
    map.has(obj)// false
    map.clear();//清空所有
    
    
  • Map 遍历操作

    const map1 = new Map();
    map1.set('aaa', 100);
    map1.set('bbb', 200);
    
    //1、keys() // 返回键名的遍历器
    for (let key of map1.keys()) { console.log(key); }
    // "aaa"
    // "bbb"
    
    //2、values() 返回值的遍历器
    for (let value of map1.values()) { console.log(value); }
    // 100
    // 200
    
    //3、entries() 返回所有成员的遍历器,item 中是一个数组,即["aaa",100]
    for (let item of map1.entries()) { console.log(item[0], item[1]); }
    //aaa 100
    //bbb 200
    
    //4、forEach 遍历所有成员
    map1.forEach((value, key) => { console.log(value, key); })
    // 100 "aaa"
    // 200 "bbb"
    
    

3、数组中的map 方法,这点要和Map() 这种数据类型做区分

  • 定义: 用于对数组中的每个元素进行处理,得到新的数组

  • 特点:不改变原有的数据的结构和数据

    const arr = [1, 2, 3, 4];
    //返回上述数组的每一项值求平方的结果
    const newArr = arr.map(item => item * item);//[1, 4, 9, 16]
    

八、JS数据类型以及Symbol 类型

1、关于JS 的基本类型:

  • JS ES5的 6 种数据类型

    即:String、Number、 Boolean、 undefined、 object、 null

    ES6 新增了一种数据类型 Symbol谷歌67版本中还出现了一种 bigInt。是指 安全存储、操作大整数

    所以目前一共有8种即:String、Number、 Boolean、 undefined、 object、 null 、Symbol、bigInt

  • 基本类型 和 引用类型

    基本类型:除object外,即 String、 Number、boolean、null、undefined
    引用类型:即object,里面包含 function、Array、Date

2、关于Symbol 类型:

说明: ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值,凡是属性名属于Symbol类型的,就是独一无二的,可以保证不会与其他属性名产生冲突。

  • 基本用法:

      let s = Symbol()
      typeof s //"symbol"
    
      //注意:Symbol 函数前面不能使用new 命令否则会报错,它是一种类似字符串的数据类型
    
  • 独一无二性(Symbol 函数的参数只是表示对当前Symbol 值得描述(独一无二性),因此相同参数的Symbol 函数返回值是不相等的。 )

      let s1 = Symbol();
      let s2 = Symbol();
      s1 === s2 // false
    
      let s1 = Symbol('foo');
      let s2 = Symbol('foo');
    
      s1 === s2 // false,即使字符串相等,但是Symbol 有独一无二性,故不相等
    
  • 不能与其他值进行运算(Symbol 值不能与其他类型的值进行运算,会报错)

      let sym = Symbol('My Symbol');
      let str1 = "your symbol is " + sym; //报错TypeError: can't convert symbol to string
    
  • 可显示转换为字符串

    let sym = Symbol('My symbol');
    sym.toString();// 'Symbol(My symbol)', 这里打印结果并不是Symbol 里面的字符串,切记
    
  • Symbol 可以转换为bool值,但不能转换为数值

    let sym = Symbol();
    Boolean(sym) // true
    Number(sym) //报错 Uncaught TypeError: Cannot convert a Symbol value to a number
    

九、类(class)的声明、继承(extend)

1、声明类与继承 class、 extend、 ES6 class 与 ES5 function的区别及联系

  • 定义:class(类) 作为对象的模板被引入,可以通过class关键字定义类。class的本质是function,它可以看做是一个语法糖,让对象原型的写法更加清晰,更像面向对象的语法。

    //1、匿名类
    let Example = class {
      constructor(a) {
        this.a = a;
      }
    }
    
    //2、命名类
    let Example = class Example {
      constructor(a) {
        this.a = a;
      }
    }
    
  • 类定义不会被提升,这意为着必须在访问前对类进行定义,否则就会报错。不可重复声明。注意类中的方法不需要用 function 关键字,方法不能加分号。

      class Example {
        constructor() {
          console.log('我是constructor');
        }
      }
    
      new Example();// 我是constructor
    
      //注意:constructor方法是类的默认方法,创建类的实例化对象时被调用,class 的实例化必须通过 new 关键字
    
  • 通过 extends 关键字实现类的继承

    • 1)继承基本语法
    class Point { }
    
    class ColorPoint extends Point {
      constructor(x, y, color) {
        super(x, y); //调用父类的 constructor(x,y),这里子类必须先调用 super,否则会报错
        this.color = color;
      }
      toString() {
        return this.color + '' + super.toString();
      }
    }
    //注意:子类必须在constructor 方法中调用 super方法,否则在新建实例,即new对象时会报错。
    //即调用super之后子类才可以使用this关键字
    
    • 2)静态方法的继承
    // 父类的静态方法,也会被子类继承
    class A { static hello() { console.log("Hello world") } }
    class B extends A { }
    B.hello(); //静态的不用new就可以调用
    
    

十、ES6 的新特性

1、ES6 有哪些新特性?

  - 1、let const 命令
  - 2、解构赋值
  - 3、字符串方法模板字符串
  - 4、默认函数、剩余函数、箭头函数
  - 5、Symbol
  - 6、Set 和 Map
  - 7、模块化
  - 8、Promise
  - 9、class继承
  - 10、数组的扩展

十一、Promise 对象

1、说明:

  • Promise 是异步编程的一种解决方案,从语法上来说,Promise 是一个对象,从它可以获取异步操作的消息。

2、特点:

  • Promise 异步操作有三种状态:

    • pending (进行中)、fullfilled (已成功)、rejected (已失败)。除了异步操作的结果,任何其他操作都无法改变这个状态。

    • Promise 对象只有从 pending 变为 fulfilled 和 从 pending变为 rejected的状态改变,只要处于 fullfilled 和 rejected,状态就不会再改变了,即resolved(已定型)

  • 示例代码:

    const p1 = new Promise(function (resolve, reject) {
      resolve('success1');
      resolve('success2');
    });
    
    const p2 = new Promise(function (resolve, reject) {
      resolve('success3');
      reject('reject');
    })
    
    p1.then(function (value) {
      console.log(value);//success1
    });
    
    p2.then(function (value) {
      console.log(value);//success3
    })
    

3、缺点:

- 无法取消 Promise, 即一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。
- 当处于pending状态时,无法得知目前进展到哪一阶段(刚刚开始还是即将完成)

十二、异步编程

1、generator(异步编程、yield、next()、await 、async)

  • 说明:

    • Generator函数,可以通过 yield 关键字,把函数的执行流挂起,为改变执行流提供了可能,从而为异步编程提供了解决方案。
    • Generator函数有区别于普通函数的地方:一是在 function后面,函数名有个*,函数内部有yield表达式。其中*用来函数为 Generator 函数,yield用来定义函数内部的状态。
  • 基本用法

      function* sendParameter() {
        console.log("start");
        var x = yield '2';
        console.log("one:" + x);
        var y = yield '3';
        console.log("two:" + y);
        console.log("total:" + (x + y));
      }
    
      var sendp1 = sendParameter();
      sendp1.next();
      // strat
      // {value: "2", done: false}
      sendp1.next();
      // one:undefined
      // {value: "3", done: false}
      sendp1.next();
      // two:undefined
      // total:NaN
      // {value: undefined, done: true}
    
  • 基本用法(传入参数)

      var sendp2 = sendParameter();
      sendp2.next(10);
      // strat
      // {value: "2", done: false}
      sendp2.next(20);// 传入的参数,会作为上一个yield的返回值
      // one:20
      // {value: "3", done: false}
      sendp2.next(30);
      // two:30
      // total:50
      // {value: undefined, done: true}
    
      //一般情况下, next 方法不传入参数的时候,yield表达式的返回值是undefined。当next传入参数的时候,该参数会作为上一步yield的返回值
      //yield* 表达式表示 yield 返回一个遍历器对象,用于在 Generator 函数内部,调用另一个 Generator 函数
    

2、Async 和 Await

  • async 是ES7 才有的与异步操作有关的关键字,和 PromiseGenerator 有很大的关联。async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。

    async function helloAsync() { return "helloAsync"; }
    
    console.log(helloAsync());// 返回Promise 对象,即: Promise {<resolved>: "helloAsync"}
    
    helloAsync().then(v => { console.log(v) });// helloAsync
    
  • await 操作符用于等待一个 Promise 对象,它只能在异步函数 async function 内部使用。返回Promise 对象的处理结果。

      function testAwait(x) {
        return new Promise(resolve => {
          setTimeout(() => {
            resolve(x);
          }, 3000);
        })
      }
    
      async function helloAsync() {
        //等待Promise对象有返回,即等待其resolve
        var x = await testAwait("hello world");
        //等到Promise 对象resolved,则会执行下面代码,即3秒后打印结果
        console.log(x);
      }
    
      //3秒后打印 hello world
      helloAsync();//hello world
    
  • 注意:

    • 如果等待的不是Promise 对象,则应该返回本身。
    • 如果一个 Promise 被传递给一个 await 操作符,await将等待Promise 正常处理完成并返回其处理结果。

十三、练习题:

1、说出下面程序的运行结果

(function (x, f = () => x) {
  var x;
  var y = x;
  x = 2;
  return [x, y, f()];

})(1) 

//[2,1,1]

//结果集第三个值为1,原因是箭头函数的作用域等于定义时的作用域。

2、说出下面程序的运行结果

(function () {
  console.log(
      [
        (() => this.x).bind({ x: "inner" })(),
        (() => this.x)()
      ]
    )
}).call({ x: "outer" })

//['outer','outer']
//原因是:箭头函数的作用域等于其定义时的作用域,所以通过 bind 设置的this是无效的

3、下面程序的执行结果为何?

  let x, { x: y = 1 } = { x }; y;
  //结果为1,首先定义 x,然后在赋值的时候,会执行一次 y=1,最后返回 y

4、 下面程序的执行结果为何?

(function () {
  let a = this ? class b { } : class c { };
  console.log(typeof a, typeof b, typeof c);
})()

// function undefined undefined
//在定义函数变量时,函数名称只能在函数体中生效

5、说出下面程序的执行结果

[...[...'...']].length //3

//扩展运算符 ... 将后面的对象转换为数组,具体做法是: [...<数据>] 
//例如 [...'abc'] 等价于 ['a','b','c']

//所以
[...'...'] // [".", ".", "."]
[...[...'...']] //[".", ".", "."]

6、如何实现对象 o3 对 对象 o2 的复制,但是只复制o2 自身的属性,不复制它的原型对象 o1 的属性?

  let o1 = { a: 1 };
  let o2 = { b: 2 };
  o2.__proto__ = o1;
  let o3 = { ...o2 };

7、async 函数有几种声明方式?

  //1、通过函数声明
  async function foo() { }

  //2、通过表达式声明
  var bar = async function () { }

  //3、通过对象声明
  var obj = { async bazfunction() { } }

  //4、通过箭头函数声明
  var fot = async () => { }

8、在async中,如何处理错误语句?

  • 1) 使用 try ...catch 包住可能会出错的部分。

      async function demo() {
        try {
          await doDemoThing();
        } catch (err) {
          console.log(err);
        }
      }
    
  • 2)对可能要出错的异步操作添加catch回调函数

      async function demo() {
        await doSomeThing().catch(err=>console.log(err))
      }
    

9、Iterator 的作用是什么?

- 1)为各种数据提供一个统一的,简便的访问接口。
- 2)使得数据结构的成员能够按某种次序排列。
- 3)ES6 创造了一种新的遍历命令 for...of 循环,Iterator 接口主要提供 for...of 使用。

10、for...in 循环的缺点是什么?

- 1)数组的键名是数字,但是 for...in循环以字符串作为键名
- 2)for...in循环不仅遍历数字键名,还好遍历手动添加的其他键,甚至包括原型链上的键。
- 3)某些情况下,for...in循环会以任意顺序遍历键名。
  总之: for...in循环会以任意顺序遍历键名。

11、请说出扩展运算符与剩余操作符之间的区别

简单的说,在某种程度上,剩余操作符和扩展运算符相反,扩展运算符会使数组“展开”成多个元素,剩余操作符会手机多个元素并“压缩”成一个单一的元素。

12、ES6 Module 能否使模块按需加载(lazyload)?

- ES6 module (import/export语句)是静态的,所以无法用于按需加载,import 是静态执行的,不能使用表达式或者变量,另外 import命令有提升的效果,提升到头部首先执行。
- 如果 if 语句中加入 import语句就会报错。
- 可以使用 import() 方法,返回一个 Promise 对象。

13、使用json 方式实现深拷贝

  let arr1 = [1, 2, 3, 4];
  let arr2 = JSON.parse(JSON.stringify(arr1));
posted @ 2021-05-05 10:21  PS-Jerry  阅读(78)  评论(0编辑  收藏  举报