javascript相关

387 阅读20分钟

作用域的理解

通过名称查找变量的一套规则,作用域是一套规则。
JavaScript中采用的是词法作用域,简单来说,作用域是由我们在书写代码时,变量所在的位置决定的。(大部分情况是这样)
欺骗词法作用域的方法:
1. eval(),在运行期间动态形式插入代码,并修改词法作用域的环境。(eval通常用来执行动态创建的代码)
2. with,根据所传递给他的对象,创建一个完全隔离的词法作用域,这个对象的所有属性也会作为标识符存在于这个作用域内。
* 不推荐使用evalwith的原因:
    1.是性能影响
        eval/with会在运行时修改或者创建新的作用域,js会在编译阶段进行一些性能优化,其中有些优化需要依赖
        静态词法分析。如果引擎发现了evalwith,在词法分析阶并不能明确知道eval会接受什么样的代码,对作用域进行怎样的修改,最悲观的情况是所有的优化都是无意义的。
        with也是,并不能知道传递给with用来创建新此法作用域的对象是什么内容
    2.在严格模式下会被影响限制(with被完全禁止)

let、const

词法作用域:作用域是由书写代码时函数、变量声明位置决定的
变量作用域提升:函数、变量的声明被提升到该函数/变量所在作用域的顶部
作用域链:当查找变量时,先从当前作用域开始查找,如果没有则查找上层作用域,一直到全局作用域这样一个过程
- var声明的变量会提升到作用域顶部。letconst声明的变量不会提升,只能在声明以后使用
- var声明的全局变量会被挂载到window对象上,letconst不会
- varlet可以重复声明一个变量,const不可以
- var声明的变量作用域范围是函数作用域,letconst声明的变量作用域范围是块级作用域
- const声明的常量,一旦声明不能再次赋值(更改对象属性可以,因为对象地址没有变)
- 垃圾收集(和闭包以及回收内存垃圾的回收机制有关,块作用域可以让js引擎知道哪些变量可以被回收掉)

在es5中实现let和const

1. 实现let:用立即执行函数的形式来定义不会被污染的变量
    (function(){
        var a = 1
        console.log(a) //1
    })()
    console.log(a) //a is not defined
2. 实现const:使用Object.defineProperty()方法实现
    var _const = function (data, value) {
        window.data = value //把要定义的data挂载到window上,并赋值
        Object.defineProperty(window, data, {
            enumerable: false,
            configurable: false,
            get: function() { return value },
            set: function(v) {
                if (v !== value) {
                    throw new TypeError('Assignment to constant variable')
                } else {
                    return value
                }
            }
        })
    }
    
    _const('a', 22)
    console.log(a)
    delete data
    console.log(a)
    a = 20 //报错
3. 使用Object.freeze()实现const: freeze方法冻结一个对象,冻结了的对象不能添加新的属性,不能删除已有属性,不能修改已有属性的可枚举性、可配置性、可写性、以及不能修改属性值,该对象的原型也不能修改
    var obj = Object.freeze({a: 11, b: 22})
    obj.a = 44 //严格模式下会报错
    console.log(obj.a) // 11

立即执行函数和普通函数

1. 最重要的区别是他们的函数名称标识符绑定在何处
    - 普通函数声明,绑定在所在作用域
    - 函数表达式绑定在函数表达式自身的函数中,在函数表达式内部可访问
2. 普通声明函数,需要显示调用才会执行
3. 立即执行函数的名称标识符会隐藏在函数自身中,不会非必要的污染外部作用域

Object.keys, Object.getOwnPropertyNames和for...in

Object.keys,对象自身的可枚举属性
Object.getOwnPropertyNames,对象自身的所有(可枚举和不可枚举)属性
for...in,对象本身以及从其构造函数原型中继承的可枚举属性(除Symbol)
in操作符,会检查属性是否在对象及其原型链上(包括可枚举和不可枚举的)
##可枚举就相当于“可以出现在对象属性的遍历中”
for/of 遍历可迭代对象,比如ArrayMapSetStringTypedArrayarguments

类型转换

== 和 ===

==宽松相等允许在比较中进行强制类型转换
===严格相等不允许

Object.is

Object.is判断两个值是否为同一个值
Object.is(NaN, NaN) //true
Object.is(+0, -0) // false
NaN == NaN //false
+0 === -0 //true
和==的区别是在判断时不会对两边值进行强制类型转换

如何让(a == 1 && a == 2 && a == 3)的值为true

1. 不严格相等时,如果一方为object,另一方为NumberString或者Symbol,会将对象转原始类型再进行比较
   let obj = {
       v: 1,
       valueOf: function() {
           return this.v++
       },
   }
2. 使用Object.defineProperty
   var v = 1
   Object.defineProperty(window, 'a', {
       get: function() {
           return v++
       }
   })

箭头函数和普通函数的区别

箭头函数
1. 不能用new关键字调用,没有prototype属性
2. 没有arguments参数
3. 没有自己的this,箭头函数的this指向上层作用域中的this

this

this的指向取决于函数调用方式,this是在运行时进行绑定的,而不是编写时进行绑定。
1. 通过new构造函数调用,永远指向构造函数返回的实例对象
2. 由call、apply、bind等方法调用,指向绑定的对象
3. 通过对象调用,this指向对象
4. 独立函数调用,非严格模式下指向window,严格模式下为undefined
5. 箭头函数没有自己的this,根据当前的词法作用域,箭头函数的this指向上层作用域中的this

typeof 和 instanceof

typeof 能判断除null之外的所有原始类型(null被判断为'object'),对于对象类型,除函数会返回'function',其他都被判断为'object'
instanceof 通过原型链判断出对象的类型,但无法判断原始类型,比如: 'abc' instanceof String
Object.prototype.toString.call() 是最优解

Symbol

symbol除数字型、字符串型、布尔型、nullundefined的第六种原始类型,是具有唯一性的特殊值,用它来命名对象属性不容易导致重名。主要用于私有或者特殊属性
所有原始类型,除了Symbol都有各自的字面形式
Symbol的作用是作为对象属性的唯一标识符,防止对象属性冲突,因为调用Symbol()方法返回值是唯一的
well-known Symbol为标准对象定义了一些以前只在语言内部可见的功能
- Symbol.hasInstance, 每一个函数都拥有该方法,用于确定对象是否为函数的实例, instanceof为该方法的简写语法
- Symbol.toPrimitive,该方法被定义在了每一个标准类型的原型上,并且规定了当对象被转换为原始值时应当执行的操作。可以自定义Symbol.toPrimitive方法覆盖默认的强制转换特性

手写instanceof

instanceOf基于原型链来实现的

function myInstanceof(left, right) {
    if(typeof left !== 'object') return false
    while(true) {
        if(left === null) return false
        if(left.__proto__ === right.prototype) return true
        left = left.__proto__
    }
    
}

原型、原型链

原型链 当试图得到对象的某一个属性时,如果对象自身不存在这个属性,那么会去他的__proto__属性中去查找(即对象的构造函数的prototype中查找),
如果没有,继续向上查找,直到顶层返回null(Object.protoType.__proto__ = null),形成一个链式结构,即原型链
- JS中每个函数都有个prototype属性
- 每个引用类型都有一个__proto__属性
- 所有引用类型的__proto__属性指向它构造函数的prototype属性
- 创建一个无原型对象
    Object.create(null, {foo: {value: 22, writable: true} })
    Object.setPrototypeOf(obj, null)
    obj.__proto__ = null
- 如何解决原型污染问题
    我们可以通过原型给对象添加属性,我们自己添加的这些属性都是可枚举的,当使用for...in循环时,通过原型添加的这些属性也会被循环到,但我们只需要对象自身属性
    解决方法:
    - 使用Object.defineProperty()定义不可枚举属性(常规的in运算符会认为原型中不可枚举的属性也存在对象中,该方法无法解决这个问题)
    - 使用对象的hasOwnProperty()方法(但是该方法可能会被用户定义的覆盖)
    - 使用无原型对象     

深拷贝和浅拷贝

浅拷贝 只复制对象一层的属性(跟赋值操作区分开)
 - 浅拷贝方法,Object.assign(), {...obj}, for...in
深拷贝 递归复制了所有层级
 - 配合使用JSON.stringify()和JSON.parse(),缺点:在序列化对象时,会忽略属性值为undefinedSymbol类型的值,函数和原型成员会被忽略,如果对象存在循环引用也无法处理
 - 简单实现深拷贝:
 function deepClone(obj) {
     function isObject(o) {
         return (typeof o === 'object' || typeof o === 'function') && o !== null
     }
     if(!isObject(obj)) {
         throw new Error('非对象')
     }
     var newObj = Array.isArray(obj) ? [...obj] : {...obj}
     //Object.Reflect.ownKeys = Object.getOwnPropertyNames().concat(Object.getOwnPropertySymbols())
     Object.Reflect.ownKeys(newObj).forEach(key => {
         newObj[key] = isObject(newObj[key]) ? deepClone(newObj[key]) : newObj[key]
     })
     return newObj
 }

闭包

当一个函数能够记住并访问它词法作用域的时候,就产生了闭包,即使函数是在词法作用域之外执行
* 无论使用何种方式将内部函数传递到所在作用域以外,它都会持有对原始定义作用域的引用(这个引用就叫作闭包),无论在何处执行这个函数都会使用闭包
闭包的几种表现形式:
- 返回一个内部函数
- 作为函数参数传递 无论通过何种方式将函数传递到他所在词法作用域之外,该函数都会保持对原有作用域的引用,无论在何处执行这个函数,都会产生闭包
- 回调函数 在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步任务中,只要使用了回调函数,实际上就在使用闭包
- 非典型闭包IIFE(立即执行函数表达式)
为什么要设计闭包? 
- 有时候需要把变量隐藏起来,让外部的程序不能访问。应用场景:设计私有的方法和变量
- 为什么不使用局部变量?局部变量只在局部有效,出了这个局部,就回收了,而有些变量需要隐藏有需要长期保持

js的垃圾回收机制

- 标记清除 js中有个全局对象,浏览器中是windows。定期的,垃圾回收器会从全局对象开始,找所有这个全局对象引用的对象,
再找这些对象引用的对象...对这些活着的对象进行标记,这是标记阶段。清除阶段就是清除那些没有被标记的对象
- 引用计数 如果没有引用指向该对象,该对象将被垃圾回收机制收回(存在问题:当循环引用时,可能会导致内存泄漏)

继承

1. 原型链继承: 通过重写子类原型,将父类的实例作为子类的原型
  缺点: 1.不能向父类构造函数传参 2.父类上的引用类型属性会被所有实例共享,改变其中一个实例时,其他实例也会受到影响 3.调用父类构造函数可能产生一些副作用
  function Animal() {
      this.colors = ['red', 'green']
  }
  function Dog(name) {
      this.name = name
  }
  Dog.prototype = new Animal()
  
  var dog1 = new Dog('韭韭')
  var dog2 = new Dog('蒜头')
  dog1.colors.push('grey')
  console.log(dog1.colors) //['red', 'green', 'grey']
  console.log(dog2.colors) //['red', 'green', 'grey']
2. 借用构造函数实现继承: 在子类中使用call()方法,实现借用父类构造函数并向父类构造函数传参的目的。
   缺点:不能继承父类原型中的属性和方法
    function Animal(name) {
        this.name = name
        this.colors = ['red', 'bule']
    }
    Animal.prototype.eat = function () {
        console.log(this.name + 'is eating')
    }
    function Dog(name) {
        Animal.call(this, name)
    }
    var dog1 = new Dog('韭韭')
    var dog2 = new Dog('蒜头')
    dog1.colors.push('green')
    console.log(dog1.colors) // ['red', 'blue', 'green']
    console.log(dog2.colors) // ['red', 'blue']
    dog1.eat() //报错
3. 组合继承:组合了原型继承和借用构造函数继承这两种方法,保留了两种继承方式的有点。
   缺点:父类构造函数被多次调用
   function Animal(name) {
       this.name = name
       this.colors = ['red', 'blue']
   }
   Animal.prototype.eat = function () {
       console.log(this.name + 'is eating')
   }
   function Dog(name) {
       Animal.call(this, name)
   }
   Dog.prototype = new Animal() //第一次调用
   var dog1 = new Dog('韭韭')
   var dog2 = new Dog('蒜头')
   dog1.colors.push('green')
   console.log(dog1.colors) //['red', 'blue', 'green']
   console.log(dog2.colors) //['red', 'blue']
4. 寄生组合继承:在组合继承的基础上,使用Object.create()来创建子类的prototype
   function Animal(name) {
       this.name = name
       this.colors = ['red', 'blue']
   }
   Animal.prototype.eat = function () {
       console.log(this.name + 'is eating')
   }
   function Dog(name) {
       Animal.call(this, name)
   }
   Dog.prototype = Object.create(Animal.prototype)
   Dog.prototype.constructor = Dog
5. class实现继承: 使用es6新特性class来实现继承
   class Animal {
       constructor(name) {
           this.name = name
           this.colors = ['red', 'blue']
       }
       function eat() {}
   }
   class Dog extends Animal {
       constructor(name) {
           super(name) //调用父类构造函数,且在出现this之前必须先调用super()
       }
   }
   
- super.xxx 等价于 Object.getPrototypeOf(this).xxx
- 为什么this一定要在super之后?
    不像基类的构造函数,派生类的构造函数没初始的this绑定,在派生类中执行super(),会生成一个this绑定,
    相当于this = new Base()
   

使用new调用函数发生了什么

1. 新建一个实例对象
2. 设置原型链
3.this指向新创建的instance对象,执行构造函数
4. 返回创建的实例对象(如果构造函数返回的是引用类型,则返回这个引用类型,否则返回创建的instance对象)
手写new关键词方法
    function myNew(constructor, ...args) {
        // 新建一个对象,并设置原型链
        const obj = Object.create(constructor.prototype)
        // 设置原型链,并执行构造函数
        const res = constructor.apply(obj, args)
        return isObject(res) ? res : obj
    }
    function isObject(obj) {
        return obj !== null && typeof obj === 'object'
    }
    //案例
    function Person(name) {
        this.name = name
    }
    const p1 = myNew(Person, '蒜头')
    console.log(p1 instanceof Person) //true
    console.log(p1.name) //蒜头
    
Tipnew.target: 当一个函数通过new关键字调用时,其内部的new.target指向函数自己,否则指向undefined

Map和Set结构

1. Map类型存储着有序键值对,其中键名和对应的值支持所有的数据类型。(跟普通对象的区别:普通对象创建的是无序键值对,对象属性只能是字符串)
   Map结构的方法或属性如下:
   - set(key, value),新增一个键值对
   - get(key),根据键获取值
   - size属性,获取Map结构的长度
   - delete(key), 删除
   - forEach(value, key, map), 遍历
   - has(key), 根据键值判断是否存在Map中
   - keys()/values()/entries()返回对应Map的键值迭代器(使用for...of遍历)
   - clear()清空Map
2. WeakMap比较特殊,只支持对象类型的键名,集合中存放的键是对象的弱引用,当该对象的其他强引用都被清除时,集合中的弱引用键以及其对应的值也会被自动垃圾回收调
   WeakMap常被应用于保存web页面的Dom元素,以及存储对象实例的私有数据
   WeakMap不支持size属性、forEach()、clear()方法
3. Set是一个集合,里面的值是唯一的,重复添加的值会被忽略
   Set的方法和属性如下:
   add(), 添加新值
   size, 获取set结构的长度
   delete(v), 根据指定的值删除
   has(v), 指定的值是否存在集合中
   keys()/values()/entries()返回对应集合的迭代器(使用for...of遍历)
   clear(), 清空set
   Set结构的扩展运用: 数组去重、并集、交集、差集
4. WeakSet构造函数只接受对象类型,不接受任何原始值
   WeakSet集合中存放的是对象的弱引用,当该对象的其他强引用都被清除时,集合中对应的弱引用会被自动垃圾回收
   WeakSet不支持size属性,forEach()、clear()方法

数组和链表的区别

数组将元素在内存中连续存放,由于每个元素占用内存相同,因而可以通过下标迅速访问数组元素,但是如果需要增删元素,则需要移动大量元数
链表,增删元素容易,只需修改指针指向。但是访问链表的元素,则需要从表头开始查找

Generator

ES6之前,函数一旦执行将不能被中断。在es6之后,有了Generator,函数可以暂停执行,等到合适的机会再执行。 生成器(generator)是一种返回迭代器的函数

async/await

如果一个方法前面加了async,该方法就会返回一个Promiseasync就是将函数用Promise.resolve()包裹了下。
await只能配合async使用,不能单独出现。

promise和async/await区别

- 写法上async/await看起来是同步代码
- 对错误处理方式不一样,promise通过then的第二个参数或者catch方法,async/await使用trycatch捕获
- async/awaitGenerator+promise的语法糖

手写Promise

- 处理异步请求的时候,有时候需要根据第一个请求的结果,再去执行第二个请求...这种嵌套多了就出现我们常说的地狱回调
- 地狱回调的负面作用有:
    - 可读性差
    - 代码耦合度高,可维护性差
    - 代码复用性差
简易promise,并不完全符合Promise/A+规范
//promise的三个状态,pending(等待)、resolved(完成)、rejected(拒绝)
const PENDING = 'PENDING'
const RESOLVED = 'RESOLVED'
const REJECTED = 'REJECTED'
function MyPromise (fn) {
    const self = this
    self.state = PENDING
    self.value = null
    self.resolvedCallbacks = []
    self.rejectedCallbacks = []
    //完成方法
    function resolve(value) {
        if(self.state === PENDING) {
            self.state = RESOLVED
            self.value = value
            self.resolvedCallbacks.forEach(cb => cb(self.value))
        }
    } 
    //拒绝方法
    function reject(value) {
        if(self.state === PENDING) {
            self.state = REJECTED
            self.value = value
            self.rejectedCallbacks.forEach(cb => cb(self.value))
        }
    }
    //执行传入的方法
    try {
         fn(resolve, reject)
    } catch(e) {
        reject(e)
    }
}

MyPromise.prototype.then = function (success, fail) {
    const self = this
    success = typeof success === 'function' ? success : (v) => { return v}
    fail = typeof fail === 'function' ? fail : (v) => { return v}
    if(self.state === PENDING) {
        self.resolvedCallbacks.push(success)
        self.rejectedCallbacks.push(fail)
    }
    if(self.state === RESOLVED) {
        success(self.value)
    }
    if(self.state === REJECTED) {
        faile(self.value)
    }
}
//执行自定义promise
new MyPromise((resolve, reject) => {
    setTimeout(() => {
        resolve(100)
    }, 2000)
}).then(v => {
    console.log(v)
}).then(v => {
    console.log(c,233)
})

promise参考:juejin.cn/post/684490… 根据promiseA+实现promise: zhuanlan.zhihu.com/p/23312442 juejin.cn/post/684490…

EventLoop事件循环

- js是单线程执行的,设计成单线程的原因在于:js可以改变dom,如果是多线程的话,一个线程要修改dom,
一个线程要删除dom,就可能不会正确渲染dom,为了避免复杂性,便设计成单线程。
    单线程的好处:1节省内存空间 2.节省上下文切换时间 3.没有锁的问题存在
- web worker允许js创建多个线程,但是子线程完全受主线程控制,且不得操作dom,所以,并没有改变js单线程的本质
- js引擎线程和UI渲染线程是互斥的,在js运行期间有可能会阻塞UI渲染,原因是,如果在js工作期间,UI还在渲染的话,有可能不会正确渲染dom。
- 进程:CPU分配资源的最小单位(是能拥有资源和独立运行的最小单位)
- 线程:CPU调度的最小单位(线程是建立在进程基础上的一次程序运行单位,一个进程可以有多个线程)

- 浏览其中js引擎的事件循环机制:
    1. 先执行执行栈中的任务,遇到异步事件挂起(异步事件执行完毕,浏览器事件表会将回调函数放到对应宏任务或者微任务列表中),
    2. 执行完执行栈中的任务,主线程空了,先去查看微任务队列中的事件,取出回调函数,放到执行栈中执行,直到微任务队列都执行完毕
    3. 浏览器进行UI渲染
    4. 检查是否有web worker任务,有则执行
    4. 执行完本轮loop,回到步骤1,直到宏任务队列和微任务队列都为空
    直到全部执行完。
宏任务:setInterval、setTimeOut、setImmediate、script(整体代码)、I/O、UI Rendering,可以有多个队列
微任务:process.nextTick、Promise.then、Object.observe、mutataionObserver,只能有一个队列
    process.nextTick优先级高于Promis.then

手写call、apply、bind方法

- call、apply设置函数内部this和函数的参数,并且调用函数返回结果,区别是apply的参数是以数组方式传入
- bind,绑定函数内部的this和部分函数传参,返回一个函数,调用该函数,继续传入参数,最终已绑定的this和参数和后面传进来的参数一起传给原函数,调用并返回值
1.手写call
    Function.prototype.myCall = function(context, ...args) {
        if(typeof this !== 'function') {
            throw new TypeError('is not function')
        }
        context = context || window
        context.fn = this
        const result = context.fn(...args)
        delete context.fn
        return result
    }
2. 手写apply
    Function.prototype.myApply = functiong(context, args) {
        if(typeof this !== 'function') {
            throw new TypeError('is not function')
        }
        context = context || window
        context.fn = this
        const result = context.fn(...args)
        delete context.fn
        return result
    }
3. 手写bind
    Function.prototype.myBind = function(context, ...bindedArgs) {
        if(typeof this !== 'function') {
            throw new TypeError('error')
        }
        const self = this
        return function F(...args) {
            if(this instanceof F) {
                return new self(...bindedArgs, ...args)
            }
            return self.apply(context, bindedArgs.concat(args))
        }
    }
    function foo(){
        console.log(this.name)
    }
    obj = {name: '蒜头'}
    var fn = foo.bind(obj)
    fn() //蒜头

手写函数节流

function throttle(fn, interval = 500) {
    let firstTime = true
    let timer = null
    return function() {
        const args = arguments
        const self = this
        if(firstTime) {
            fn.apply(self, args)
            firstTime = false
        }
        if(timer) return
        timer = setTimeout(() => {
            clearTimeout(timer)
            timer = null
            fn.apply(self, args)
        }, interval)
    }
}
//使用
window.onresize = throttle(function () {
    console.log('onresize')
}, 500)

手写函数防抖

延迟某个方法的执行,指定的时间后才执行
function debounce(fn, delay) {
    let timer = null
    return function () {
        const self = this
        if(timer) {
            clearTimeout(timer)
        }
        timer = setTimeout(() => {
            fn.apply(self, arguments)
        }, delay)
    }
}

数组去重方法

1. Array.from(new Set(arr))
2. [...new Set(arr)]
3. 利用对象属性去重
   function unique(arr) {
       if(!Array.isArray(arr)) {
           throw new TypeError('is not Array')
       }
       let obj = {}
       let result = []
       for(var i = 0; i < arr.length; i++) {
           if(!obj[arr[i]]) {
               result.push(arr[i])
               obj[arr[i]] = 1
           } else {
               obj[arr[i]]++
           }
       }
       return result
   }

手写基于发布/订阅的事件系统

事件系统包括on(监听)、off(取消)、emit(触发)、once(绑定一次)几个方法

const event ={
    subs: {},
    on(event, cb) {
        if(!this.subs[event]) {
                this.subs[event] = []
        }
        this.subs[event].push(cb)
    },
    off(event, cb) {
        if(!this.subs[event]) throw new Error('事件未注册')
        if(cb) {
            let len = this.subs[event].length
            for(i =0 ; i < len; i++) {
                if(cb === this.subs[event][i]) {
                    this.subs[event].splice(i, 1)
                    if(this.subs[event].length === 0)  this.subs[event] = null
                    break
                }
                }
        } else {
            this.subs[event] = null
        }
    },
    emit(event, ...args) {
        if(!this.subs[event]) throw new Error('事件未注册')
        this.subs[event].forEach(cb => {
            cb(...args)
        });
    }
}


手写getQueryString

MVC和MVVC

1. MVC模式的意思是软件可以分为三个部分:
- 视图(View): 用户界面
- 控制器(controller): 业务逻辑
- 模型(model): 数据保存
各部分之间的通信是单向的,View传递指令给Controller,Controller完成业务逻辑,要求Model改变状态,Model将新的数据发送到View,用户得到反馈。
2. MVVM
- View代表视图层
- Model代表数据层
- ViewModel是一个同步View和Model的对象,通过数据双向绑定,将View和Model层连接起来,View和Model之间的同步工作完全是自动的,开发者只需要关注业务逻辑,不需要关注数据状态同步的问题。

Proxy能干什么

用于创建对象的代理,从而实现对基本操作的拦截和自定义,如属性查找、赋值、枚举、函数调用等
const p = new Proxy(target, handler)

CommonJS、ESModule、AMD

CommonJS是Node.js环境中模块系统,模块是同步加载的(意味着模块在被引用时,代码就会执行,然后再继续执行引用它的代码),适用于服务器端;
ESModule是新的官方标准,支持静态分析,加载可以是同步的或者异步的,取决于模块的类型;
AMD加载是异步的,可以支持按需加载,有助于提高性能,适用于浏览器环境加载脚本;
ESModule已逐渐成为浏览器和Node.js中的标准模块系统

- ES6模块输出的值的引用,CommonJS输出的是值的浅拷贝
- es6是静态引入,编译时引用;CommonJS是动态引入,执行时引用
- 只有es6才能静态分析,实现treeShaking

模块

字面量和内置对象(比如String、Number)构造出来的对象的区别

原始值"i am a string"并不是一个对象,它只是一个字面量,如果要在这个字面量上执行一些操作,比如获取长度,访问某个字符,需要将其转换为String对象。引擎会在需要的时候自动转换。
-  var s1 = 'aaaa'
   typeof s1;  //'string'
   s1 instanceof String; //false
-  var s2 = new String('bbbb')
   typeof s2; //'object'
   s2 instanceof String; //true

对象的不可变性

1. 结合writable:falseconfigurable:false 就可以创建一个常量属性(不可修改,删除重定义)
2. Object.preventExtendsions(), 禁止一个对象添加新属性
3. Object.seal(),实际上调用了Object.preventExtendsions(),并对属性设置了configurable:false
4. Object.freeze(),调用了Object.seal(),并把所有属性标记为writable:false

几种设计模式