es6基础知识以及常考面试题

327 阅读5分钟

var、let、const区别

1. var存在提升,可以在声明之前使用。let、const因为暂时死区的原因,不能在声明之前使用

例子1:

console.log(a); // 输出: undefiend
var a = 10; 

console.log(b); // 输出:报错 ReferenceError: b is not defined
let b = 20;

function foo() {
    console.log(test);
    let test = 'test'
}
foo(); // Uncaught ReferenceError: Cannot access 'test' before initialization

使用var声明的变量在编译的时候,会将声明提升,编译后如下,所以console输出的结果是10。

var a;
console.log(a);
a = 10;

函数提升优于变量提升,函数提升把整个函数挪到了作用域的顶部,变量提升只会声明挪到作用域顶部

console.log(foo); // 输出:function foo
var foo = 123;
function foo () {}
console.log(foo); // 输出:123

console.log(foo1); // 输出:function foo1
function foo1 () {}
var foo1 = 123;
console.log(foo1); // 输出:123

从上面代码可以看出,用函数方式声明的函数 在编译的时候会被整体提升,所以在第一次console的结果都是函数,第二次输出的为变量的值。编译后的两块大致如下

// 第一段代码
var foo;
function foo() {};
console.log(foo);
foo = 123console.log(foo);

// 第二段代码
var foo1;
function foo1() {};
console.log(foo1);
foo1 = 123console.log(foo1);

大家是否疑惑过为什么存在变量提升这个东西,多麻烦呀,但是存在即合理,提升根本原始是为了解决函数相互调用的情况,如下代码

function test1() {
    test2();
}
function test2() {
    test1();
}
test1();

在上述代码中,如果没有变量提升,是无法实现的。 报错是因为存在暂时死区,不能在声明变量前进行访问。
3. const和let基本一致,const声明的基础类型的数据无法变更,声明的对象类型可以变更属性的值
4. 全局中使用var声明的变量会挂挂载window上,let和const不会

var a = 1;
let b = 2;
const c = 3;
console.log(window.a, window.b, window.c); // 输出:1 undefined undefiend

原型继承和class继承

JS中不存在类,class只是语法糖,其本质是函数

class Person {};
Person instanceof Function; // true

组合继承

function Parent(name) {
    this.name = name
}
Parent.prototype.getName = function() {
    console.log(this.name);
}
function Child(name) {
    Parent.call(this, name);
}
Child.prototype = new Parent();
let child = new Child('捌');
child instanceof Parent; // true
child.getName(); // '捌'

组合继承的核心在于子类的构造函数中Parent.call(this, name);继承父类的属性,然后改变子类的原型为 new Parent()来继承父类的函数。
组合继承有点在于构造函数可以传参,不会和父类共享引用数据类型,但可以共享父类的函数。但存在一个缺点是在继承父类函数的时候调用了父类的构造函数,导致子类原型上多了父类的属性,存在内存上的浪费。
image.png

寄生组合继承

这种方式是对组合继承的优化,而组合继承的缺点在于继承父类方法的时候调用了构造函数,我们只需要优化了这点就行。

function Parent(name) {
    this.name = name;
}
Parent.prototype.getName = function() {
    console.log(this.name);
}
function Child(name) {
    Parent.call(this, name);
}
Child.prototype = Object.create(Parent.prototype, {
    constructor: {
        value: Child,
        enumerable: false,
        writable: true,
        configurable: true
    }
});
let child = new Child();
child instanceof Parent // true

image.png

class 继承

组合继承和寄生组合继承都是通过原型继承,在es6中,我们可以直接通过class解决。

class Parent {
    constructor(name) {
        this.name = name;
    }
    getName() {
        console.log(this.name);
    }
}
class Child extends Parent {
    constructor(name) {
        super(name);
        this.name = name;
    } 
}
let child = new Child('捌');
child.getName(); // 捌
child instanceof Parent; // true

class继承核心在与子类使用extends表明继承那个父类,并在子类中调用super,这代码可以看成 Parent.call(this, name);

模块化

面试题:什么是模块化?都有哪几种方式?各有什么特点

好处:

  • 解决命名冲突
  • 提供复用性
  • 提高代码可维护性

立即执行函数(方式1)

早期,立即执行函数是实现模块化最常见的手段,通过函数作用域解决各种命名冲突,污染全局作用域的问题

(function(data) {
    console.log(t);
    // 进行各种声明和操作
})(globlaData)

AMD 和 CMD

// AMD
defined('./a', './b', function(a, b) {
    // 模块加载完成后可以使用
    a.do();
    b.do();
});

// CMD
defined(function(require, exports, module) {
    // 加载模块,可以把require卸载任意地方,使用模块的延迟加载
    let a = reqire('./a');
    a.do();  
})

CommonJS

commonJs最早是在node中使用,目前仍然使用广泛,我们在webpack中也会经常看到它。

// a.js
module.exports = {
    name: 'commonJs'
}
// 或者
exports.name = 'commonJs'

// b.js
const module = require('./a.js');
console.log(module.name); // commonJs

commonJs的实现

const module = require('./a.js');
module.name;
// 这里其实包装了一层立即执行函数,防止对全局变量造成影响

// 这里的module是Node中的一个独有变量
module.exports = {
    name: 'commonJs'
}

// module的实现
let module = {
    id: 'XXX',
    exports: {
        name: 'commonJs'
    }
}
// exports 和 module。exports共用一个内存地址,所以用法一样
let exports = module.exports;

let load = function(module) {
    // 导出的内容
    var name = 'commonJs';
    module.exports = name;
    return module.exports;
}
// 当require的时候,去找到独特的id对应的内容,然后将使用的内容用立即执行函数包装下。

exportsmodule.exports用法相同的原因是 共用了一个内存地址,所以不能直接对exports直接进行赋值,如果直接赋值会导致 和module.exports内存地址不一致,修改并不会修改module.exports的值。

ES Module

与CommonJS的区别

  • CommonJS支持动态导入,例如const module = require(path + '/a.js'), ES Module目前不支持
  • CommonJS 是同步加载,因为用于服务端,文件就在本地,同步导入,即使卡住对主线程影响也不大。后者是异步导入,因为用于浏览器,需要下载文件,如果也才用同步导入会对渲染造成很大的影响
  • CommonJS 导入都是值拷贝,就算导出的值改变了,前面导入的值也不会改变,如果需要更新,需要再重新导入。ES Module采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入的值会随导出的值变化。
  • ES Module会变编译成reuire/exports来执行
// 导出模块儿
export function foo () {};
export default function() {};

// 导入模块
import {foo} from './a';
import foo from './a';

Proxy

vue3讲使用Proxy代码Object.defineProperty来实现响应式。Proxy是es6中新增的功能,可以用来定义对象。

let obj = new Proxy(target, handler);

target表示添加代理的对象,handler用来自定义对象中的操作,比如setget函数

接下来用Proxy实现一个响应式

let onWatch = (obj, setBind, getLogger) => {
    const handler = {
        set(obj, property, value, receiver) {
            console.log('receiver==', receiver);
            setBind(property, value);
            return Reflect.set(obj, property, value);
        },
        get(obj, property, receiver) {
            getLogger(obj, property);
            return Reflect.get(obj, property, receiver);
        }
    }
    return new Proxy(obj, handler);
}
const obj = { age: 16 };
const p = onWatch(
    obj,
    (property, value) => {
        console.log(`监听到${property}变更为${value}`);
    },
    (obj, property) => {
        console.log(`${property} = ${obj[property]}`);
    }
)
p.age // a = 16
p.age = 24 // 监听到age变更为24