javascript 拥有一套设计良好的规则来存储变量,而且以后能够方便地找到这些变量,这套规则被称为做用域。javascript
做用域就是代码的执行环境,全局执行环境就是全局做用域,函数的执行环境就是私有做用域,它们都是栈内存。前端
当代码在一个环境中执行时,会建立变量对象的一个做用域链(做用域造成的链条),因为变量的查找是沿着做用域链来实现的,因此也称做用域链为变量查找的机制。java
内部环境能够经过做用域链访问全部外部环境,但外部环境不能访问内部环境的任何变量和函数。
编译node
以 var a = 2;为例,说明 javascript 的内部编译过程,主要包括如下三步:react
分词(tokenizing)ajax
把由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元(token)编程
var a = 2;被分解成为下面这些词法单元:var、a、=、二、;。这些词法单元组成了一个词法单元流数组json
[ "var": "keyword", "a": "identifier", "=": "assignment", "2": "integer", ";": "eos" (end of statement) ]
解析(parsing)api
把词法单元流数组转换成一个由元素逐级嵌套所组成的表明程序语法结构的树,这个树被称为“抽象语法树” (Abstract Syntax Tree, AST)数组
var a = 2;的抽象语法树中有一个叫 VariableDeclaration 的顶级节点,接下来是一个叫 Identifier(它的值是 a)的子节点,以及一个叫 AssignmentExpression 的子节点,且该节点有一个叫 Numericliteral(它的值是 2)的子节点
{ operation: "=", left: { keyword: "var", right: "a" } right: "2" }
将 AST 转换为可执行代码的过程被称为代码生成
var a=2;的抽象语法树转为一组机器指令,用来建立一个叫做 a 的变量(包括分配内存等),并将值 2 储存在 a 中
实际上,javascript 引擎的编译过程要复杂得多,包括大量优化操做,上面的三个步骤是编译过程的基本概述
任何代码片断在执行前都要进行编译,大部分状况下编译发生在代码执行前的几微秒。javascript 编译器首先会对 var a=2;这段程序进行编译,而后作好执行它的准备,而且一般立刻就会执行它
执行
简而言之,编译过程就是编译器把程序分解成词法单元(token),而后把词法单元解析成语法树(AST),再把语法树变成机器指令等待执行的过程
实际上,代码进行编译,还要执行。下面仍然以 var a = 2;为例,深刻说明编译和执行过程
编译
依据编译器的编译原理,javascript 中的重复声明是合法的
// test在做用域中首次出现,因此声明新变量,并将20赋值给test var test = 20 // test在做用域中已经存在,直接使用,将20的赋值替换成30 var test = 30
执行
查询
在引擎执行的第一步操做中,对变量 a 进行了查询,这种查询叫作 LHS 查询。实际上,引擎查询共分为两种:LHS 查询和 RHS 查询
从字面意思去理解,当变量出如今赋值操做的左侧时进行 LHS 查询,出如今右侧时进行 RHS 查询
更准确地讲,RHS 查询与简单地查找某个变量的值没什么区别,而 LHS 查询则是试图找到变量的容器自己,从而能够对其赋值
function foo(a) { console.log(a) // 2 } foo(2)
这段代码中,总共包括 4 个查询,分别是:
一、foo(...)对 foo 进行了 RHS 引用
二、函数传参 a = 2 对 a 进行了 LHS 引用
三、console.log(...)对 console 对象进行了 RHS 引用,并检查其是否有一个 log 的方法
四、console.log(a)对 a 进行了 RHS 引用,并把获得的值传给了 console.log(...)
嵌套
在当前做用域中没法找到某个变量时,引擎就会在外层嵌套的做用域中继续查找,直到找到该变量,或抵达最外层的做用域(也就是全局做用域)为止
function foo(a) { console.log(a + b) } var b = 2 foo(2) // 4
行 RHS 引用,没有找到;接着,引擎在全局做用域中查找 b,成功找到后,对其进行 RHS 引用,将 2 赋值给 b
异常
为何区分 LHS 和 RHS 是一件重要的事情?由于在变量尚未声明(在任何做用域中都没法找到变量)的状况下,这两种查询的行为不同
RHS
// 对b进行RHS查询时,没法找到该变量。也就是说,这是一个“未声明”的变量 function foo(a) { a = b } foo() // ReferenceError: b is not defined
function foo() { var b = 0 b() } foo() // TypeError: b is not a function
LHS
function foo() { a = 1 } foo() console.log(a) // 1
function foo() { 'use strict' a = 1 } foo() console.log(a) // ReferenceError: a is not defined
原理
function foo(a) { console.log(a) } foo(2)
以上面这个代码片断来讲明做用域的内部原理,分为如下几步:
【1】引擎须要为 foo(...)函数进行 RHS 引用,在全局做用域中查找 foo。成功找到并执行
【2】引擎须要进行 foo 函数的传参 a=2,为 a 进行 LHS 引用,在 foo 函数做用域中查找 a。成功找到,并把 2 赋值给 a
【3】引擎须要执行 console.log(...),为 console 对象进行 RHS 引用,在 foo 函数做用域中查找 console 对象。因为 console 是个内置对象,被成功找到
【4】引擎在 console 对象中查找 log(...)方法,成功找到
【5】引擎须要执行 console.log(a),对 a 进行 RHS 引用,在 foo 函数做用域中查找 a,成功找到并执行
【6】因而,引擎把 a 的值,也就是 2 传到 console.log(...)中
【7】最终,控制台输出 2
编译器的第一个工做阶段叫做分词,就是把由字符组成的字符串分解成词法单元。这个概念是理解词法做用域的基础
简单地说,词法做用域就是定义在词法阶段的做用域,是由写代码时将变量和块做用域写在哪里来决定的,所以当词法分析器处理代码时会保持做用域不变
不管函数在哪里被调用,也不管它如何被调用,它的词法做用域都只由函数被声明时所处的位置决定
function foo(a) { var b = a * 2 function bar(c) { console.log(a, b, c) } bar(b * 3) } foo(2) // 2 4 12
在这个例子中有三个逐级嵌套的做用域。为了帮助理解,能够将它们想象成几个逐级包含的气泡
做用域气泡由其对应的做用域块代码写在哪里决定,它们是逐级包含的
气泡 1 包含着整个全局做用域,其中只有一个标识符:foo
气泡 2 包含着 foo 所建立的做用域,其中有三个标识符:a、bar 和 b
气泡 3 包含着 bar 所建立的做用域,其中只有一个标识符:c
做用域气泡的结构和互相之间的位置关系给引擎提供了足够的位置信息,引擎用这些信息来查找标识符的位置
在代码片断中,引擎执行 console.log(...)声明,并查找 a、b 和 c 三个变量的引用。它首先从最内部的做用域,也就是 bar(...)函数的做用域开始查找。引擎没法在这里找到 a,所以会去上一级到所嵌套的 foo(...)的做用域中继续查找。在这里找到了 a,所以引擎使用了这个引用。对 b 来说也同样。而对 c 来讲,引擎在 bar(...)中找到了它
[注意]词法做用域查找只会查找一级标识符,若是代码引用了 foo.bar.baz,词法做用域查找只会试图查找 foo 标识符,找到这个变量后,对象属性访问规则分别接管对 bar 和 baz 属性的访问
foo = { bar: { baz: 1 } } console.log(foo.bar.baz) // 1
做用域查找从运行时所处的最内部做用域开始,逐级向外或者说向上进行,直到碰见第一个匹配的标识符为止
在多层的嵌套做用域中能够定义同名的标识符,这叫做“遮蔽效应”,内部的标识符“遮蔽”了外部的标识符
var a = 0 function test() { var a = 1 console.log(a) // 1 } test()
全局变量会自动为全局对象的属性,所以能够不直接经过全局对象的词法名称,而是间接地经过对全局对象属性的引用来对其进行访问
var a = 0 function test() { var a = 1 console.log(window.a) //0 } test()
经过这种技术能够访问那些被同名变量所遮蔽的全局变量。但非全局的变量若是被遮蔽了,不管如何都没法被访问到
javascript 使用的是词法做用域,它最重要的特征是它的定义过程发生在代码的书写阶段
那为何要介绍动态做用域呢?实际上动态做用域是 javascript 另外一个重要机制 this 的表亲。做用域混乱多数是由于词法做用域和 this 机制相混淆,傻傻分不清楚
动态做用域并不关心函数和做用域是如何声明以及在任何处声明的,只关心它们从何处调用。换句话说,做用域链是基于调用栈的,而不是代码中的做用域嵌套
var a = 2 function foo() { console.log(a) } function bar() { var a = 3 foo() } bar()
【1】若是处于词法做用域,也就是如今的 javascript 环境。变量 a 首先在 foo()函数中查找,没有找到。因而顺着做用域链到全局做用域中查找,找到并赋值为 2。因此控制台输出 2
【2】若是处于动态做用域,一样地,变量 a 首先在 foo()中查找,没有找到。这里会顺着调用栈在调用 foo()函数的地方,也就是 bar()函数中查找,找到并赋值为 3。因此控制台输出 3
两种做用域的区别,简而言之,词法做用域是在定义时肯定的,而动态做用域是在运行时肯定的
执行栈,在其余编程语言中也被叫作调用栈,具备 LIFO(后进先出)结构,用于存储在代码执行期间建立的全部执行上下文。
当 JavaScript 引擎首次读取你的脚本时,它会建立一个全局执行上下文并将其推入当前的执行栈。每当发生一个函数调用,引擎都会为该函数建立一个新的执行上下文并将其推到当前执行栈的顶端。
引擎会运行执行上下文在执行栈顶端的函数,当此函数运行完成后,其对应的执行上下文将会从执行栈中弹出,上下文控制权将移到当前执行栈的下一个执行上下文。
让咱们经过下面的代码示例来理解这一点:
let a = 'Hello World!'; function first() { console.log('Inside first function'); second(); console.log('Again inside first function'); } function second() { console.log('Inside second function'); } first(); console.log('Inside Global Execution Context');
当上述代码在浏览器中加载时,JavaScript 引擎会建立一个全局执行上下文而且将它推入当前的执行栈。当调用 first()
函数时,JavaScript 引擎为该函数建立了一个新的执行上下文并将其推到当前执行栈的顶端。
当在 first()
函数中调用 second()
函数时,Javascript 引擎为该函数建立了一个新的执行上下文并将其推到当前执行栈的顶端。当 second()
函数执行完成后,它的执行上下文从当前执行栈中弹出,上下文控制权将移到当前执行栈的下一个执行上下文,即 first()
函数的执行上下文。
当 first()
函数执行完成后,它的执行上下文从当前执行栈中弹出,上下文控制权将移到全局执行上下文。一旦全部代码执行完毕,Javascript 引擎把全局执行上下文从执行栈中移除。
到目前为止,咱们已经看到了 JavaScript 引擎如何管理执行上下文,如今就让咱们来理解 JavaScript 引擎是如何建立执行上下文的。
执行上下文分两个阶段建立: 1)建立阶段; 2)执行阶段
在任意的 JavaScript 代码被执行前,执行上下文处于建立阶段。在建立阶段中总共发生了三件事情:
所以,执行上下文能够在概念上表示以下:
ExecutionContext = { ThisBinding = <this value>, LexicalEnvironment = { ... }, VariableEnvironment = { ... }, }
This Binding:
在全局执行上下文中, this
的值指向全局对象,在浏览器中, this
的值指向 window 对象。
在函数执行上下文中, this
的值取决于函数的调用方式。若是它被一个对象引用调用,那么 this
的值被设置为该对象,不然 this
的值被设置为全局对象或 undefined
(严格模式下)。例如:
let person = { name: 'peter', birthYear: 1994, calcAge: function() { console.log(2018 - this.birthYear); } } person.calcAge(); // 'this' 指向 'person', 由于 'calcAge' 是被 'person' 对象引用调用的。 let calculateAge = person.calcAge; calculateAge(); // 'this' 指向全局 window 对象,由于没有给出任何对象引用
官方 ES6 文档将词法环境定义为:
词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符与特定变量和函数的关联关系。词法环境由环境记录(environment record)和可能为空引用(null)的外部词法环境组成。
简而言之,词法环境是一个包含 标识符变量映射 的结构。(这里的 标识符 表示变量/函数的名称, 变量 是对实际对象【包括函数类型对象】或原始值的引用)
在词法环境中,有两个组成部分:(1) 环境记录(environment record) (2) 对外部环境的引用
词法环境有两种类型:
this
的值指向这个全局对象。注意:对于 函数环境 而言, 环境记录 还包含了一个 arguments
对象,该对象包含了索引和传递给函数的参数之间的映射以及传递给函数的参数的 长度(数量) 。例如,下面函数的 arguments
对象以下所示:
function foo(a, b) { var c = a + b; } foo(2, 3); // arguments 对象 Arguments: {0: 2, 1: 3, length: 2},
环境记录一样有两种类型(以下所示):
抽象地说,词法环境在伪代码中看起来像这样:
GlobalExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 标识符绑定在这里 outer: <null> } } } FunctionExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 标识符绑定在这里 outer: <Global or outer function environment reference> } } }
它也是一个词法环境,其 EnvironmentRecord
包含了由 VariableStatements 在此执行上下文建立的绑定。
如上所述,变量环境也是一个词法环境,所以它具备上面定义的词法环境的全部属性。
在 ES6 中, LexicalEnvironment 组件和 VariableEnvironment 组件的区别在于前者用于存储函数声明和变量( let
和 const
)绑定,然后者仅用于存储变量( var
)绑定。
让咱们结合一些代码示例来理解上述概念:
let a = 20; const b = 30; var c; function multiply(e, f) { var g = 20; return e *f *g; } c = multiply(20, 30);
执行上下文以下所示:
GlobalExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 标识符绑定在这里 a: < uninitialized >, b: < uninitialized >, multiply: < func > } outer: <null> }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", // 标识符绑定在这里 c: undefined, } outer: <null> } } FunctionExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 标识符绑定在这里 Arguments: {0: 20, 1: 30, length: 2}, }, outer: <GlobalLexicalEnvironment> }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // 标识符绑定在这里 g: undefined }, outer: <GlobalLexicalEnvironment> } }
注意:只有在遇到函数 multiply
的调用时才会建立函数执行上下文。
你可能已经注意到了 let
和 const
定义的变量没有任何与之关联的值,但 var
定义的变量设置为 undefined
。
这是由于在建立阶段,代码会被扫描并解析变量和函数声明,其中函数声明存储在环境中,而变量会被设置为 undefined
(在 var
的状况下)或保持未初始化(在 let
和 const
的状况下)。
这就是为何你能够在声明以前访问 var
定义的变量(尽管是 undefined
),但若是在声明以前访问 let
和 const
定义的变量就会提示引用错误的缘由。
这就是咱们所谓的变量提高。
这是整篇文章中最简单的部分。在此阶段,完成对全部变量的分配,最后执行代码。
注:在执行阶段,若是 Javascript 引擎在源代码中声明的实际位置找不到 let
变量的值,那么将为其分配 undefined
值。
Node.js 才支持这个特性,经过 Error.captureStackTrace 来实现,Error.captureStackTrace 接收一个 object 做为第 1 个参数,以及可选的 function 做为第 2 个参数。其做用是捕获当前的调用栈并对其进行裁剪,捕获到的调用栈会记录在第 1 个参数的 stack 属性上,裁剪的参照点是第 2 个参数,也就是说,此函数以前的调用会被记录到调用栈上面,而以后的不会。
让咱们用代码来讲明,首先,把当前的调用栈捕获并放到 myObj 上:
const myObj = {}; function c() {} function b() { // 把当前调用栈写到 myObj 上 Error.captureStackTrace(myObj); c(); } function a() { b(); } // 调用函数 a a(); // 打印 myObj.stack console.log(myObj.stack); // 输出会是这样 // at b (repl:3:7) <-- Since it was called inside B, the B call is the last entry in the stack // at a (repl:2:1) // at repl:1:1 <-- Node internals below this line // at realRunInThisContextScript (vm.js:22:35) // at sigintHandlersWrap (vm.js:98:12) // at ContextifyScript.Script.runInThisContext (vm.js:24:12) // at REPLServer.defaultEval (repl.js:313:29) // at bound (domain.js:280:14) // at REPLServer.runBound [as eval] (domain.js:293:12) // at REPLServer.onLine (repl.js:513:10)
上面的调用栈中只有 a -> b,由于咱们在 b 调用 c 以前就捕获了调用栈。如今对上面的代码稍做修改,而后看看会发生什么:
const myObj = {}; function d() { // 咱们把当前调用栈存储到 myObj 上,可是会去掉 b 和 b 以后的部分 Error.captureStackTrace(myObj, b); } function c() { d(); } function b() { c(); } function a() { b(); } // 执行代码 a(); // 打印 myObj.stack console.log(myObj.stack); // 输出以下 // at a (repl:2:1) <-- As you can see here we only get frames before b was called // at repl:1:1 <-- Node internals below this line // at realRunInThisContextScript (vm.js:22:35) // at sigintHandlersWrap (vm.js:98:12) // at ContextifyScript.Script.runInThisContext (vm.js:24:12) // at REPLServer.defaultEval (repl.js:313:29) // at bound (domain.js:280:14) // at REPLServer.runBound [as eval] (domain.js:293:12) // at REPLServer.onLine (repl.js:513:10) // at emitOne (events.js:101:20)
在这段代码里面,由于咱们在调用 Error.captureStackTrace 的时候传入了 b,这样 b 以后的调用栈都会被隐藏。
如今你可能会问,知道这些到底有啥用?若是你想对用户隐藏跟他业务无关的错误堆栈(好比某个库的内部实现)就能够试用这个技巧。
当程序运行出现错误时, 一般会抛出一个 Error 对象. Error 对象能够做为用户自定义错误对象继承的原型.
Error.prototype 对象包含以下属性:
constructor–指向实例的构造函数
message–错误信息
name–错误的名字(类型)
上述是 Error.prototype 的标准属性, 此外, 不一样的运行环境都有其特定的属性. 在例如 Node, Firefox, Chrome, Edge, IE 10+, Opera 以及 Safari 6+
这样的环境中, Error 对象具有 stack 属性, 该属性包含了错误的堆栈轨迹. 一个错误实例的堆栈轨迹包含了自构造函数以后的全部堆栈结构.
只查看调用栈:console.trace
a() function a() { b() } function b() { c() } function c() { let aa = 1 } console.trace()
在 JavaScript 中,函数也是对象,所以函数能够做为一个对象的属性,此时该函数被称为该对象的方法,在使用这种调用方式时,this 被天然绑定到该对象
var test = { a:0, b:0, get:function(){ return this.a; } }
函数也能够直接被调用,此时 this 绑定到全局对象。在浏览器中,window 就是该全局对象。好比下面的例子:函数被调用时,this 被绑定到全局对象,
接下来执行赋值语句,至关于隐式的声明了一个全局变量,这显然不是调用者但愿的。
function makeNoSense(x) { this.x = x; }
javaScript 支持面向对象式编程,与主流的面向对象式编程语言不一样,JavaScript 并无类(class)的概念,而是使用基于原型(prototype)的继承方式。
相应的,JavaScript 中的构造函数也很特殊,若是不使用 new 调用,则和普通函数同样。做为又一项约定俗成的准则,构造函数以大写字母开头,
提醒调用者使用正确的方式调用。若是调用正确,this 绑定到新建立的对象上。
function Point(x, y){ this.x = x; this.y = y; }
让咱们再一次重申,在 JavaScript 中函数也是对象,对象则有方法,apply 和 call 就是函数对象的方法。
这两个方法异常强大,他们容许切换函数执行的上下文环境(context),即 this 绑定的对象。
不少 JavaScript 中的技巧以及类库都用到了该方法。让咱们看一个具体的例子:
function Point(x, y){ this.x = x; this.y = y; this.moveTo = function(x, y){ this.x = x; this.y = y; } } var p1 = new Point(0, 0); var p2 = {x: 0, y: 0}; p1.moveTo(1, 1); p1.moveTo.apply(p2, [10, 10])
由于函数内部声明 的变量是局部的,只能在函数内部访问到,可是函数外部的变量是对函数内部可见的,这就是做用域链的特色了。
子级能够向父级查找变量,逐级查找,找到为止
所以咱们能够在函数内部再建立一个函数,这样对内部的函数来讲,外层函数的变量都是可见的,而后咱们就能够访问到他的变量了。
function bar(){ //外层函数声明的变量 var value=1; function foo(){ console.log(value); } return foo(); }; var bar2=bar; //实际上bar()函数并无由于执行完就被垃圾回收机制处理掉 //这就是闭包的做用,调用bar()函数,就会执行里面的foo函数,foo这时就会访问到外层的变量 bar2();
foo()包含bar()内部做用域的闭包,使得该做用域可以一直存活,不会被垃圾回收机制处理掉,这就是闭包的做用,以供foo()在任什么时候间进行引用。
function addFn(a,b){ return(function(){ console.log(a+"+"+b); }) } var test =addFn(a,b); setTimeout(test,3000);
通常setTimeout的第一个参数是个函数,可是不能传值。若是想传值进去,能够调用一个函数返回一个内部函数的调用,将内部函数的调用传给setTimeout。内部函数执行所需的参数,外部函数传给他,在setTimeout函数中也能够访问到外部函数。
在一些编程软件中,好比c语言中,须要使用malloc来申请内存空间,再使用free释放掉,须要手动清除。而js中是有本身的垃圾回收机制的,通常经常使用的垃圾收集方法就是标记清除。
标记清除法:在一个变量进入执行环境后就给它添加一个标记:进入环境,进入环境的变量不会被释放,由于只要执行流进入响应的环境,就可能用到他们。当变量离开环境后,则将其标记为“离开环境”。
一、当自执行函数在循环当中使用时,自执行函数会在循环结束以后才会运行。好比你在自执行函数外面定义一个数组,在自执行函数当中给这个数组追加内容,你在自执行函数以外输出时,会发现这个数组当中什么都没有,这就是由于自执行函数会在循环运行完后才会执行。
二、当自执行函数在循环当中使用时,要是自执行函数当中嵌套ajax,那么循环当中的下标i就不会传进ajax当中,须要在ajax外面把下标i赋值给一个变量,在ajax中直接调用这个变量就能够了。
例子:
$.ajax({ type: "GET", dataType: "json", url: "***", success: function(data) { //console.log(data); for (var i = 0; i < data.length; i++) { (function(i, abbreviation) { $.ajax({ type: "GET", url: "/api/faults?abbreviation=" + encodeURI(abbreviation), dataType: "json", success: function(result) { //获取数据后作的事情 } }) })(i, data[i].abbreviation); } } });
所谓的递归函数就是在函数体内调用本函数。使用递归函数必定要注意,处理不当就会进入死循环。
const asyncDeal = (i) = > { if (i < 3) { $.get('/api/changeParts/change_part_standard?part=' + data[i].change_part_name, function(res) { //获取数据后作的事情 i++; asyncDeal(i); }) } else { //异步完成后作的事情 } }; asyncDeal(0);
async/await更加语义化,async 是“异步”的简写,async function 用于申明一个 function 是异步的; await,能够认为是async wait的简写, 用于等待一个异步方法执行完成;
async/await是一个用同步思惟解决异步问题的方案(等结果出来以后,代码才会继续往下执行)
能够经过多层 async function 的同步写法代替传统的callback嵌套
自动将常规函数转换成Promise,返回值也是一个Promise对象
只有async函数内部的异步操做执行完,才会执行then方法指定的回调函数
异步函数内部可使用await
await 放置在Promise调用以前,await 强制后面点代码等待,直到Promise对象resolve,获得resolve的值做为await表达式的运算结果
await只能在async函数内部使用,用在普通函数里就会报错
const asyncFunc = function(i) { return new Promise(function(resolve) { $.get(url, function(res) { resolve(res); }) }); } const asyncDeal = async function() { for (let i = 0; i < data.length; i++) { let res = await asyncFunc(i); //获取数据后作的事情 } } asyncDeal();
容许模块经过require方法来同步加载所要依赖的其余模块,而后经过exports或module.exports来导出须要暴露的接口。
使用方式:
// 导入 require("module"); require("../app.js"); // 导出 exports.getStoreInfo = function() {}; module.exports = someValue;
优势:
缺点:
为何浏览器不能使用同步加载,服务端能够?
参照CommonJs模块表明node.js的模块系统
采用异步方式加载模块,模块的加载不影响后面语句的运行。全部依赖模块的语句,都定义在一个回调函数中,等到加载完成以后,回调函数才执行。
使用实例:
// 定义 define("module", ["dep1", "dep2"], function(d1, d2) {...}); // 加载模块 require(["module", "../app"], function(module, app) {...});
加载模块require([module], callback);第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback是加载成功以后的回调函数。
优势:
缺点:
实现AMD规范表明require.js
RequireJS对模块的态度是预执行。因为 RequireJS 是执行的 AMD 规范, 所以全部的依赖模块都是先执行;即RequireJS是预先把依赖的模块执行,至关于把require提早了
RequireJS执行流程:
CMD规范和AMD很类似,简单,并与CommonJS和Node.js的 Modules 规范保持了很大的兼容性;在CMD规范中,一个模块就是一个文件。
定义模块使用全局函数define,其接收 factory 参数,factory 能够是一个函数,也能够是一个对象或字符串;
factory 是一个函数,有三个参数,function(require, exports, module):
实例:
define(function(require, exports, module) { var a = require('./a'); a.doSomething(); // 依赖就近书写,何时用到何时引入 var b = require('./b'); b.doSomething(); });
优势:
缺点:
// AMD define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好 a.doSomething() // 此处略去 100 行 b.doSomething() ... }); // CMD define(function(require, exports, module) { var a = require('./a') a.doSomething() // 此处略去 100 行 var b = require('./b') // 依赖能够就近书写 b.doSomething() // ... });
(function (window, factory) { if (typeof exports === 'object') { module.exports = factory(); } else if (typeof define === 'function' && define.amd) { define(factory); } else { window.eventUtil = factory(); } })(this, function () { //module ... });
使用方式:
// 导入 import "/app"; import React from “react”; import { Component } from “react”; // 导出 export function multiply() {...}; export var year = 2018; export default ... ...
优势:
缺点:
require使用与CommonJs规范,import使用于Es6模块规范;因此二者的区别实质是两种规范的区别;
CommonJS:
ES6模块
最后:require/exports 是必要通用且必须的;由于事实上,目前你编写的 import/export 最终都是编译为 require/exports 来执行的。