1. 异步操作
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><!-- `在这里插入代码片`1. 单线程模型:单线程模型指的是,JavaScript 只在一个线程上运行。```也就是说,JavaScript同时只能执行一个任务,其他任务都必须在后面排队等待```。注意,JavaScript 只在一个线程上运行,不代表 JavaScript 引擎只有一个线程。事实上,JavaScript 引擎有多个线程,```单个脚本只能在一个线程上运行(称为主线程)```,其他线程都是在后台配合。JavaScript 之所以采用单线程,而不是多线程,跟历史有关系。JavaScript 从诞生起就是单线程,原因是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。如果 JavaScript 同时有两个线程,一个线程在网页 DOM 一开始就是单线程,这已经成了这门语言的核心特征,将来也不会改变。这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段 JavaScript 代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。```JavaScript 语言本身并不慢,慢的是读写外部数据,比如等待 Ajax 请求返回结果。这个时候,如果对方服务器迟迟没有响应,或者网络不通畅,就会导致脚本的长时间停滞。```如果排队是因为计算量大,CPU 忙不过来,倒也算了,但是很多时候 CPU 是闲着的,因为 IO 操作(输入输出)很慢(比如 Ajax 操作从网络读取数据),不得不等着结果出来,再往下执行。JavaScript 语言的设计者意识到,这时 CPU 完全可以不管 IO 操作,挂起处于等待中的任务,先运行排在后面的任务。等到 IO 操作返回了结果,再回过头,把挂起的任务继续执行下去。这种机制就是 JavaScript 内部采用的“事件循环”机制(Event Loop)。单线程模型虽然对 JavaScript 构成了很大的限制,但也因此使它具备了其他语言不具备的优势。如果用得好,JavaScript 程序是不会出现堵塞的,这就是为什么 Node 可以用很少的资源,应付大流量访问的原因。为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。--><!-- 2. 同步任务和异步任务:(1) 同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务; (2) 异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“堵塞”效应; (3) 举例来说,Ajax 操作可以当作同步任务处理,也可以当作异步任务处理,由开发者决定。如果是同步任务,主线程就等着 Ajax 操作返回结果,再往下执行;如果是异步任务,主线程在发出 Ajax 请求以后,就直接往下执行,等到 Ajax 操作有了结果,主线程再执行对应的回调函数。--><!-- 3. 任务队列和事件循环(1) JavaScript运行时,```除了一个正在运行的主线程,引擎还提供一个任务队列```(task queue),```里面是各种需要当前程序处理的异步任务```。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。)(2) 首先,```主线程会去执行所有的同步任务```。```等到同步任务全部执行完,就会去看任务队列里面的异步任务```。```如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了```。```等到执行完,下一个异步任务再进入主线程开始执行```。```一旦任务队列清空,程序就结束执行```。(3) ```异步任务的写法通常是回调函数```。```一旦异步任务重新进入主线程,就会执行对应的回调函数```。```如果一个异步任务没有回调函数,就不会进入任务队列```,```也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作```。(4) JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢? 答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做```事件循环```(Event Loop)。维基百科的定义是:“事件循环是一个程序结构,用于等待和发送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”。--><!-- 4. 异步操作的模式(1) 回调函数(2) 事件监听(3) 发布/订阅... ...-->
</body></html>
2. 定时器
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><script>/*1. setTimeout(): setTimeout还允许更多的参数。它们将依次传入推迟执行的函数(回调函数); setTimeout还允许更多的参数。它们将依次传入推迟执行的函数(回调函数); 下面代码中:最后那两个参数,将在1000毫秒之后回调函数执行时,作为回调函数的参数; */var timerId = setTimeout(function(num1, num2){console.log(num1 + num2);}, 1000, 2, 6);console.log("timerId:" + timerId);</script><script>/*2. 如果回调函数是对象的方法,那么setTimeout使得方法内部的this关键字指向全局环境,而不是定义时所在的那个对象; (1)下面代码输出的是undefined,而不是2。因为当object.y在1000毫秒后运行时,this所指向的已经不是obj了,而是全局环境;(2)解决方案:使用bind,将object.y这个方法绑定在object上面; */var object = {x: 2,test: function() {console.log(this.x); }};setTimeout(object.test, 1000); // undefinedsetTimeout(object.test.bind(object), 1000); // 2</script><script>/*3. (1) setInterval指定的是“开始执行”之间的间隔,并不考虑每次任务执行本身所消耗的时间; 因此实际上,两次执行之间的间隔会小于指定的时间。比如,setInterval指定每 100ms 执行一次,每次执行需要 5ms,那么第一次执行结束后95毫秒,第二次执行才会开始执行; 如果某次执行耗时特别长,比如需要105毫秒,那么它结束后,下一次执行就会立即开始; (2) 为了确保两次执行之间有固定的间隔,可以不用setInterval,而是每次执行结束后,使用setTimeout指定下一次执行的具体时间; */var num = 1;var timer = setTimeout(function f() {// ... ... timer = setTimeout(f, 2000);}, 2000);</script><script>/* 4. 再看一个setInterval的例子: setInterval(function () {console.log(2);}, 1000);sleep(3000); 上面代码中,setInterval要求每隔1000毫秒,就输出一个2。但是,紧接着的sleep语句需要3000毫秒才能完成; 那么setInterval就必须推迟到3000毫秒之后才开始生效; 注意,生效后setInterval不会产生累积效应,即不会一下子输出三个2,而是只会输出一个2; */</script><script>/*5. setTimeout(f, 0):setTimeout的作用是将代码推迟到指定时间执行,如果指定时间为0,即setTimeout(f, 0),那么会立刻执行吗?答案是不会。因为上一节说过,必须要等到当前脚本的同步任务,全部处理完以后,才会执行setTimeout指定的回调函数f; 也就是说,setTimeout(f, 0)会在下一轮事件循环一开始就执行; */setTimeout(function() {console.log(11);}, 0);console.log(22);/* // 22 11; 上面代码先输出22,再输出11。因为22是同步任务,在本轮事件循环执行,而11是下一轮事件循环执行; 总之,setTimeout(f, 0)这种写法的目的是,尽可能早地执行f,但是并不能保证立刻就执行f; */</script>
</body>
</html>
3. JS解释器
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><!-- JavaScript解释器的作用,是执行JavaScript源码。它通常可以包含四个组成部分: a. 词法分析器(Lexical Analyser); b. 句法解释器(Syntax Parser); c. 字节码生成器(Bytecode generator); d. 字节码解释器(Bytecode interpreter); -->
</head>
<body><!-- 1. 词法分析器词法分析器的作用,是将一行行的源码拆解成一个个词义单位(token)。所谓“词义单位”,指的是语法上不可能再分的、最小的单个字符或字符组合。首先,词法分析器会扫描(scanning)代码,提取词义单位;然后,会进行评估(evaluating),判断词义单位属于哪一类的值。var sum = 30; // 词法分析后的结果["var" : "keyword","sum" : "identifier","=" : "assignment","30" : "integer",";" : "eos" (end of statement)]上面代码中,源代码经过词法分析后,返回一组词义单位,以及它们各自的词类。--><!-- 2. 句法解析器句法解析器的作用,是将上一步生成的数组,根据语法规则,转为抽象语法树(Abstract Syntax Tree,简称AST)。如果源码符合语法规则,这一步就会顺利完成,生成一个抽象语法树;如果源码存在语法错误,这一步就会终止,抛出一个“语法错误”。{operation: "=",left: {keyword: "var",right: "sum"}right: "30"}上面代码中,抽象语法树的一个节点是赋值操作符(=),它两侧的词义单位,分别成左侧子节点和右侧子节点。通常,这一步是整个JavaScript代码执行过程中最慢的。--><!-- 3. 字节码生成器字节码生成器的作用,是将抽象语法树转为JavaScript引擎可以执行的二进制代码。目前,还没有统一的JavaScript字节码的格式标准,每种JavaScript引擎都有自己的字节码格式。最简单的做法,就是将语义单位翻成对应的二进制命令。--><!-- 4. 字节码生成器字节码解释器的作用是读取并执行字节码。-->
</body>
</html>
4. 严格模式
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><!-- 除了正常的运行模式,JavaScript 还有第二种运行模式:严格模式(strict mode)。顾名思义,这种模式采用更加严格的 JavaScript 语法。同样的代码,在正常模式和严格模式中,可能会有不一样的运行结果。一些在正常模式下可以运行的语句,在严格模式下将不能运行。-->
</head><body><script>/*1. 设计目的:早期的JavaScript语言有很多设计不合理的地方,但是为了兼容以前的代码,又不能改变老的语法,只能不断添加新的语法,引导程序员使用新语法; 严格模式是从 ES5 进入标准的,主要目的有以下几个:a. 明确禁止一些不合理、不严谨的语法,减少 JavaScript 语言的一些怪异行为; b. 增加更多报错的场合,消除代码运行的一些不安全之处,保证代码运行的安全; c. 提高编译器效率,增加运行速度; d. 为未来新版本的 JavaScript 语法做好铺垫; 总之,严格模式体现了 JavaScript 更合理、更安全、更严谨的发展方向; 2. 启用方法:进入严格模式的标志,是一行字符串 'use strict' ; 老版本的引擎会把它当作一行普通字符串,加以忽略。新版本的引擎就会进入严格模式; 严格模式可以用于整个脚本,也可以只用于单个函数; */</script><script>/*3. 显式报错:严格模式使得JavaScript的语法变得更严格,更多的操作会显式报错。其中有些操作,在正常模式下只会默默地失败,不会报错; 3.1 只读属性不可写(1) 严格模式下,设置字符串的length属性,会报错; 正常模式下,改变length属性是无效的,但不会报错; 'use strict';'abc'.length = 5; // TypeError: Cannot assign to read only property 'length' of string 'abc'(2) 严格模式下,对只读属性赋值,或者删除不可配置(non-configurable)属性都会报错; // 对只读属性赋值会报错'use strict';Object.defineProperty({}, 'a', {value: 37,writable: false});obj.a = 123; // TypeError: Cannot assign to read only property 'a' of object #<Object>// 删除不可配置的属性会报错'use strict';var obj = Object.defineProperty({}, 'p', {value: 1,configurable: false});delete obj.p // TypeError: Cannot delete property 'p' of #<Object>3.2 只设置了取值器的属性不可写(1) 严格模式下,对一个只有取值器(getter)、没有存值器(setter)的属性赋值,会报错; 'use strict';var obj = {get v() { return 1; }};obj.v = 2; // Uncaught TypeError: Cannot set property v of #<Object> which has only a getter// 上面代码中,obj.v只有取值器,没有存值器,对它进行赋值就会报错; 3.3 禁止扩展的对象不可扩展(1) 严格模式下,对禁止扩展的对象添加新属性,会报错; 'use strict';var obj = {};Object.preventExtensions(obj);obj.v = 1; // Uncaught TypeError: Cannot add property v, object is not extensible3.4 eval、arguments 不可用作标识名(1) 严格模式下,使用eval或者arguments作为标识名,将会报错。下面的语句都会报错:'use strict';var eval = 17;var arguments = 17;var obj = { set p(arguments) { } };try { } catch (arguments) { }function x(eval) { }function arguments() { }var y = function eval() { };var f = new Function('arguments', "'use strict'; return 17;");// SyntaxError: Unexpected eval or arguments in strict mod3.5 函数不能有重名的参数(1) 正常模式下,如果函数有多个重名的参数,可以用arguments[i]读取。严格模式下,这属于语法错误; function f(a, a, b) {'use strict';return a + b;}// Uncaught SyntaxError: Duplicate parameter name not allowed in this context3.6 禁止八进制的前缀0表示法(1) 正常模式下,整数的第一位如果是0,表示这是八进制数,比如0100等于十进制的64。严格模式禁止这种表示法,整数第一位为0,将报错; 'use strict';var n = 0100; // Uncaught SyntaxError: Octal literals are not allowed in strict mode. */</script><script>/*4. 增强的安全措施严格模式增强了安全保护,从语法上防止了一些不小心会出现的错误; 4.1 全局变量显式声明(1) 正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,全局变量必须显式声明; 'use strict';v = 1; // 报错,v未声明for (i = 0; i < 2; i++) { // 报错,i 未声明}function f() {x = 123;}f() // 报错,未声明就创建一个全局变量因此,严格模式下,变量都必须先声明,然后再使用; 4.2 禁止 this 关键字指向全局对象(1) 正常模式下,函数内部的this可能会指向全局对象,严格模式禁止这种用法,避免无意间创造全局变量; // 正常模式function f() {console.log(this === window);}f() // true// 严格模式function f() {'use strict';console.log(this === undefined);}f() // true上面代码中,严格模式的函数体内部this是undefined; (2) 严格模式下,函数直接调用时(不使用new调用),函数内部的this表示undefined(未定义),因此可以用call、apply和bind方法,将任意值绑定在this上面。正常模式下,this指向全局对象,如果绑定的值是非对象,将被自动转为对象再绑定上去,而null和undefined这两个无法转成对象的值,将被忽略。// 正常模式function fun() {return this;}fun() // windowfun.call(2) // Number {2}fun.call(true) // Boolean {true}fun.call(null) // windowfun.call(undefined) // window// 严格模式'use strict';function fun() {return this;}fun() // undefinedfun.call(2) // 2fun.call(true) // truefun.call(null) // nullfun.call(undefined) // undefined4.3 禁止使用 fn.callee、fn.caller(1) 函数内部不得使用fn.caller、fn.arguments,否则会报错。这意味着不能在函数内部得到调用栈了; function f1() {'use strict';f1.caller; // 报错f1.arguments; // 报错}f1();4.4 禁止使用 arguments.callee、arguments.caller(1) arguments.callee和arguments.caller是两个历史遗留的变量,从来没有标准化过,现在已经取消了。正常模式禁止删除变量下调用它们没有什么作用,但是不会报错。严格模式明确规定,函数内部使用arguments.callee、arguments.caller将会报错。4.5 禁止删除变量(1) 严格模式下无法删除变量,如果使用delete命令删除一个变量,会报错。只有对象的属性,且属性的描述对象的configurable属性设置为true,才能被delete命令删除; 'use strict';var x;delete x; // 语法错误var obj = Object.create(null, {x: {value: 1,configurable: true}});delete obj.x; // 删除成功*/</script><script>/*5. 静态绑定:JavaScript 语言的一个特点,就是允许“动态绑定”,即某些属性和方法到底属于哪一个对象,不是在编译时确定的,而是在运行时(runtime)确定的; 严格模式对动态绑定做了一些限制。某些情况下,只允许静态绑定。也就是说,属性和方法到底归属哪个对象,必须在编译阶段就确定。这样做有利于编译效率的提高,也使得代码更容易阅读,更少出现意外; 具体来说,涉及以下几个方面:5.1 禁止使用 with 语句(1) 严格模式下,使用with语句将报错。因为with语句无法在编译时就确定,某个属性到底归属哪个对象,从而影响了编译效果; 'use strict';var v = 1;var obj = {};with (obj) {v = 2;}// Uncaught SyntaxError: Strict mode code may not include a with statement5.2 创设 eval 作用域(1) 正常模式下,JavaScript 语言有两种变量作用域(scope):全局作用域和函数作用域。严格模式创设了第三种作用域:eval作用域。(2) 正常模式下,eval语句的作用域,取决于它处于全局作用域,还是函数作用域。严格模式下,eval语句本身就是一个作用域,不再能够在其所运行的作用域创设新的变量了,也就是说,eval所生成的变量只能用于eval内部。(function () {'use strict';var x = 2;console.log(eval('var x = 5; x')) // 5console.log(x) // 2})(); 上面代码中,由于eval语句内部是一个独立作用域,所以内部的变量x不会泄露到外部;(3) 注意,如果希望eval语句也使用严格模式,有两种方式:a. function f1(str){'use strict';return eval(str);}f1('undeclared_variable = 1'); // 报错b. function f2(str){return eval(str);}f2('"use strict";undeclared_variable = 1') // 报错上面两种写法,eval内部使用的都是严格模式; 5.3 arguments 不再追踪参数的变化(1) 变量arguments代表函数的参数。严格模式下,函数内部改变参数与arguments的联系被切断了,两者不再存在联动关系;正常模式下,arguments对象可以在运行时修改; 严格模式下,arguments对象是一个只读对象,修改它是无效的,但不会报错; function f(a) {a = 2;return [a, arguments[0]];}f(1); // 正常模式为[2, 2]function f(a) {'use strict';a = 2;return [a, arguments[0]];}f(1); // 严格模式为[2, 1]上面代码中,改变函数的参数,不会反应到arguments对象上来; */</script><script>/*6. 向下一个版本的 JavaScript 过渡:JavaScript语言的下一个版本是 ECMAScript 6,为了平稳过渡,严格模式引入了一些 ES6 语法; 6.1 非函数代码块不得声明函数(1) ES6 会引入块级作用域。为了与新版本接轨,ES5 的严格模式只允许在全局作用域或函数作用域声明函数。也就是说,不允许在非函数的代码块内声明函数; 'use strict';if (true) {function f1() { } // 语法错误}for (var i = 0; i < 5; i++) {function f2() { } // 语法错误}上面代码在if代码块和for代码块中声明了函数,ES5 环境会报错; 注意,如果是 ES6 环境,上面的代码不会报错,因为 ES6 允许在代码块之中声明函数; 6.2 保留字(1) 为了向将来 JavaScript 的新版本过渡,严格模式新增了一些保留字(implements、interface、let、package、private、protected、public、static、yield等)。使用这些词作为变量名将会报错; function package(protected) { // 语法错误'use strict';var implements; // 语法错误}*/</script>
</body>
</html>
5. JS与有限状态机
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=\, initial-scale=1.0"><title>Document</title>
</head>
<body><!-- 1. 概述:有限状态机(Finite-state machine)是一个非常有用的模型,可以模拟世界上大部分事物。简单说,它有三个特征:(1) 状态总数是有限的; (2) 任一时刻, 只处在一种状态之中; (3) 某种条件下, 会从一种状态转变到另一种状态; 它对JavaScript的意义在于,很多对象可以写成有限状态机; /*举例来说,网页上有一个菜单元素。鼠标点击,菜单显示;鼠标再次点击,菜单隐藏。如果使用有限状态机描述,就是这个菜单只有两种状态(显示和隐藏),鼠标会引发状态转变。var menu = {// 当前状态currentState: 'hide',// 绑定事件initialize: function() {var self = this;self.on("click", self.transition);},// 状态转换transition: function(event){switch(this.currentState) {case "hide":this.currentState = 'show';doSomething();break;case "show":this.currentState = 'hide';doSomething();break;default:console.log('Invalid State!');break;}}};*/可以看到,有限状态机的写法,逻辑清晰,表达力强,有利于封装事件。一个对象的状态越多、发生的事件越多,就越适合采用有限状态机的写法;另外,JavaScript语言是一种异步操作特别多的语言,常用的解决方法是指定回调函数,但这样会造成代码结构混乱、难以测试和除错等问题; 有限状态机提供了更好的办法:把异步操作与对象的状态改变挂钩,当异步操作结束的时候,发生相应的状态改变,由此再触发其他操作; 这要比回调函数、事件监听、发布/订阅等解决方案,在逻辑上更合理,更易于降低代码的复杂度; --><!-- 2. Javascript Finite State Machine函数库下面介绍一个有限状态机的函数库Javascript Finite State Machine。这个库非常好懂,可以帮助我们加深理解,而且功能一点都不弱; 该库提供一个全局对象StateMachine,使用该对象的create方法,可以生成有限状态机的实例; var fsm = StateMachine.create(); 生成的时候,需要提供一个参数对象,用来描述实例的性质。比如,交通信号灯(红绿灯)可以这样描述:var fsm = StateMachine.create({initial: 'green',events: [{ name: 'warn', from: 'green', to: 'yellow' },{ name: 'stop', from: 'yellow', to: 'red' },{ name: 'ready', from: 'red', to: 'yellow' },{ name: 'go', from: 'yellow', to: 'green' },]});交通信号灯的初始状态(initial)为green,events属性是触发状态改变的各种事件,比如warn事件使得green状态变成yellow状态,stop事件使得yellow状态变成red状态等等; 生成实例以后,就可以随时查询当前状态:fsm.current : 返回当前状态; fsm.is(s) : 返回一个布尔值,表示状态s是否为当前状态; fsm.can(e) : 返回一个布尔值,表示事件e是否能在当前状态触发; fsm.cannot(e) :返回一个布尔值,表示事件e是否不能在当前状态触发; Javascript Finite State Machine允许为每个事件指定两个回调函数,以warn事件为例:onbeforewarn:在warn事件发生之前触发; onafterwarn(可简写成onwarn) :在warn事件发生之后触发; 同时,它也允许为每个状态指定两个回调函数,以green状态为例:onleavegreen :在离开green状态时触发; onentergreen(可简写成ongreen) :在进入green状态时触发; 假定warn事件使得状态从green变为yellow,上面四类回调函数的发生顺序如下:onbeforewarn → onleavegreen → onenteryellow → onafterwarn; 除了为每个事件和状态单独指定回调函数,还可以为所有的事件和状态指定通用的回调函数:onbeforeevent : 任一事件发生之前触发; onleavestate : 离开任一状态时触发; onenterstate : 进入任一状态时触发; onafterevent : 任一事件结束后触发; -->
</body>
</html>