你不知道的-JSONstringfy

http://thecodebarbarian.com/t...写在前面JSON.stringfy()是将一个 JavaScript 对象转化为 JSON 格式字符串的标准方式。许多 JavaScript 框架在其内部,都会使用 JSON.stringify():Express的 res.json()、Axios的 post,以及webpack stats,它们都调用了 JSON.stringify() 方法,并包含错误案例。 译者注:这篇文章已经被翻译过多次了,但是我觉得写的太好了,所以就再翻译一次吧,当作是加深印象。 简单入门所有的现代 JavaScript 运行时都支持 JSON.stringify(),甚至 IE8 都支持它。下面是一个将简单对象转化为 JSON 的例子: const obj = { answer: 42 };const str = JSON.stringify(obj);str; // '{"answer":42}'typeof str; // 'string'你可能经常看到 JSON.stringify() 和 JSON.parse() 一起配合使用的场景,就像下面的代码一样,这种模式是实现深拷贝的方式之一: const obj = { answer: 42 };const clone = JSON.parse(JSON.stringify(obj));clone.answer; // 42clone === obj; // false错误以及边界用例JSON.stringify() 在转化对象存在循环引用时,会抛出错误。更简单地说,就是如果一个对象有一个属性指向它本身,JSON.stringify() 会抛出错误,比如: const obj = {};// 存在循环引用的对象,它指向它本身obj.prop = obj;// 会抛出 "TypeError: TypeError: Converting circular structure to JSON" 异常JSON.stringify(obj);这是 JSON.stringify() 会抛出异常的唯一情况,除非你通过声明自定义的 toJSON() 方法或者 replacer 函数。尽管如此,你仍然应该将 JSON.stringify() 包含在 try/catch 语句中,因为循环引用在实践中十分常见。 ...

June 1, 2019 · 2 min · jiezi

重读你不知道的JS (上) 第一节五章

你不知道的JS(上卷)笔记你不知道的 JavaScriptJavaScript 既是一门充满吸引力、简单易用的语言,又是一门具有许多复杂微妙技术的语言,即使是经验丰富的 JavaScript 开发者,如果没有认真学习的话也无法真正理解它们.上卷包括俩节:作用域和闭包this 和对象原型作用域和闭包希望 Kyle 对 JavaScript 工作原理每一个细节的批判性思 考会渗透到你的思考过程和日常工作中。知其然,也要知其所以然。作用域闭包启示秘诀: JavaScript中闭包无处不在,你只需要能够识别并拥抱它。闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识的创建闭包。闭包的创建和使用在你的代码中随处可见。你缺少的是根据你自己的意愿来识别、拥抱和影响闭包的思维环境。实质问题当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。词法作用域的查找规则是闭包的一部分。function foo() { var a = 2; function bar() { console.log( a ); // 2 } bar();}foo();纯学术的角度上说,上述代码片段中,函数 bar() 具有一个涵盖 foo() 作用域的闭包 (事实上,涵盖了它能访问的所有作用域,比如全局作用域)。也可以认为 bar() 被封闭在了 foo() 的作用域中。为什么呢?原因简单明了,因为 bar() 嵌套在 foo() 内部。闭包使得函数可以继续访问定义时的词法作用域。无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用 域的引用,无论在何处执行这个函数都会使用闭包。思考:function foo() { var a = 2; function baz() { console.log( a, b ); // 2 , b能获取到1吗? } bar( baz );}function bar(fn) { var b = 1; fn(); // 妈妈快看呀,这就是闭包!}常见的闭包场景function wait(message) { setTimeout( function timer() { // timer函数由引擎调用,但是已经超出了wait作用域,所以存在闭包 console.log( message ); }, 1000 );}wait( “Hello, closure!” );在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使 用了回调函数,实际上就是在使用闭包!IIFE闭包var a = 2;(function IIFE() { console.log( a );})();尽管 IIFE 本身并不是观察闭包的恰当例子,但它的确创建了闭包,并且也是最常用来创建 可以被封闭起来的闭包的工具。因此 IIFE 的确同闭包息息相关,即使本身并不会真的使用 闭包。循环和闭包for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i1000 );}延迟函数的回调会在循环结束时才执行。事实上, 当定时器运行时即使每个迭代中执行的是setTimeout(.., 0),所有的回调函数依然是在循 环结束后才会被执行,因此会每次输出一个 6 出来。缺陷是我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个 i 的副本。但是 根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的, 但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。这样说的话,当然所有函数共享一个 i 的引用。它需要有自己的变量,用来在每个迭代中储存 i 的值:for (var i=1; i<=5; i++) { (function() { // IIFE 每次执行都会立即创建一个词法上的函数作用域 var j = i; // 闭包作用域的变量j, 立即得到i的值 setTimeout( function timer() { console.log( j ); // 访问闭包作用域的变量j }, j1000 ); })();}变体:for (var i=1; i<=5; i++) { (function(j) { // IIFE 每次执行都会立即创建一个词法上的函数作用域 // 闭包作用域的变量j, 参数传递立即得到i的值 setTimeout( function timer() { console.log( j ); // 访问闭包作用域的变量j }, j1000 ); })(i);}块作用域和闭包let 声明,可以用来劫 持块作用域,并且在这个块作用域中声明一个变量。而上面的IIFE创建一个闭包,本质上这是将一个块转换成一个可以被关闭的作用域。结合块级作用域与闭包:for (let i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i1000 );}模块function CoolModule() { var something = “cool”; var another = [1, 2, 3]; function doSomething() { console.log( something ); } function doAnother() { console.log( another.join( " ! " ) ); } return { doSomething1: doSomething, doAnother: doAnother };}var foo = CoolModule(); foo.doSomething1(); // coolfoo.doAnother(); // 1 ! 2 ! 3这个模式在 JavaScript 中被称为模块。最常见的实现模块模式的方法通常被称为模块暴露, 这里展示的是其变体。doSomething() 和 doAnother() 函数具有涵盖模块实例内部作用域的闭包(通过调用 CoolModule() 实现)。当通过返回一个含有属性引用的对象的方式来将函数传递到词法作 用域外部时,我们已经创造了可以观察和实践闭包的条件。模块模式需要具备两个必要条件。必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块 实例)。封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并 且可以访问或者修改私有的状态。一个具有函数属性的对象本身并不是真正的模块。从方便观察的角度看,一个从函数调用 所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块。模块模式另一个简单但强大的变化用法是,命名将要作为公共 API 返回的对象:在上述模块中,dosomething1被作为模块内部dosomething的公开访问名。现代的模块机制var MyModules = (function Manager() { // 模块 管理器/依赖加载器 var modules = {}; function define(name, deps, impl) { for (var i=0; i<deps.length; i++) { deps[i] = modules[deps[i]]; } modules[name] = impl.apply( impl, deps ); } function get(name) { return modules[name]; } return { define: define, get: get };})();// module bar 定义MyModules.define( “bar”, [], function() { function hello(who) { return “Let me introduce: " + who; } return { hello: hello };});// module foo 定义 依赖 bar 模块MyModules.define( “foo”, [“bar”], function(bar) { var hungry = “hippo”; function awesome() { console.log( bar.hello( hungry ).toUpperCase() ); } return { awesome: awesome };});var bar = MyModules.get( “bar” );var foo = MyModules.get( “foo” );console.log(bar.hello( “hippo” )); // Let me introduce: hippo foo.awesome(); // LET ME INTRODUCE: HIPPO未来的模块机制基于函数的模块并不是一个能被稳定识别的模式(编译器无法识别),它们 的 API 语义只有在运行时才会被考虑进来。因此可以在运行时修改一个模块 的 API。相比之下,ES6 模块 API 更加稳定(API 不会在运行时改变)。由于编辑器知 道这一点,因此可以在(的确也这样做了)编译期检查对导入模块的 API 成 员的引用是否真实存在。如果 API 引用并不存在,编译器会在运行时抛出一 个或多个“早期”错误,而不会像往常一样在运行期采用动态的解决方案。ES6 的模块没有“行内”格式,必须被定义在独立的文件中(一个文件一个模块)。浏览 器或引擎有一个默认的“模块加载器”(可以被重载,但这远超出了我们的讨论范围)可 以在导入模块时异步地加载模块文件。小结闭包就好像从 JavaScript 中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人 才能够到达那里。但实际上它只是一个标准,显然就是关于如何在函数作为值按需传递的 词法环境中书写代码的。当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时 就产生了闭包。如果没能认出闭包,也不了解它的工作原理,在使用它的过程中就很容易犯错,比如在循 环中。但同时闭包也是一个非常强大的工具,可以用多种形式来实现模块等模式。模块有两个主要特征:(1)为创建内部作用域而调用了一个包装函数;(2)包装函数的返回 值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭 包。现在我们会发现代码中到处都有闭包存在,并且我们能够识别闭包然后用它来做一些有用 的事! ...

February 2, 2019 · 2 min · jiezi

重读你不知道的JS (上) 第一节四章

你不知道的JS(上卷)笔记你不知道的 JavaScriptJavaScript 既是一门充满吸引力、简单易用的语言,又是一门具有许多复杂微妙技术的语言,即使是经验丰富的 JavaScript 开发者,如果没有认真学习的话也无法真正理解它们.上卷包括俩节:作用域和闭包this 和对象原型作用域和闭包希望 Kyle 对 JavaScript 工作原理每一个细节的批判性思 考会渗透到你的思考过程和日常工作中。知其然,也要知其所以然。提升作用域同其中的变量声明出现的位置有某种微妙的联系案例1a = 2;var a;console.log( a ); // 在不了解声明提升的情况下,你可能得出结果是undefined? 实际是2案例2console.log( a ); // 你可能由于案例1的影响,得出2,或者未声明便使用,ReferenceError异常,实际上输出 undefinedvar a = 2;看看编译器怎么说:正确的思考思路是,包括变量和函数在内的所有声明都会在任何代码被执行前首先 被处理。编译 -> 解释js代码 -> 执行只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。如果提升改变了代码执行的顺序,会造成非常严重的破坏。函数声明会被提升,但是函数表达式却不会被提升。foo(); // 不是 ReferenceError, 而是 TypeError!var foo = function bar() { // …};foo()调用执行时,foo 此时并没有赋值(如果它是一个函数声明而不 是函数表达式,那么就会赋值)。foo() 由于对 undefined 值进行函数调用而导致非法操作, 因此抛出 TypeError 异常。函数优先函数声明和变量声明都会被提升,但是函数会首先被提升,然后才是变量。一个普通块内部的函数声明通常会被提升到所在作用域的顶部,这个过程不会像下面的代 码暗示的那样可以被条件判断所控制:foo(); // “b"var a = true; if (a) { function foo() { console.log(“a”); }}else { function foo() { console.log(“b”); }}小结我们习惯将var a = 2;看作一个声明,而实际上JavaScript引擎并不这么认为。它将var a和 a = 2 当作两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务。这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。 可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的 最顶端,这个过程被称为提升。声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升。 要注意避免重复声明,特别是当普通的 var 声明和函数声明混合在一起的时候,否则会引起很多危险的问题! ...

February 2, 2019 · 1 min · jiezi

重读你不知道的JS (上) 第一节三章

你不知道的JS(上卷)笔记你不知道的 JavaScriptJavaScript 既是一门充满吸引力、简单易用的语言,又是一门具有许多复杂微妙技术的语言,即使是经验丰富的 JavaScript 开发者,如果没有认真学习的话也无法真正理解它们.上卷包括俩节:作用域和闭包this 和对象原型作用域和闭包希望 Kyle 对 JavaScript 工作原理每一个细节的批判性思 考会渗透到你的思考过程和日常工作中。知其然,也要知其所以然。函数作用域和块作用域正如我们在第 2 章中讨论的那样,作用域包含了一系列的“气泡”,每一个都可以作为容 器,其中包含了标识符(变量、函数)的定义。这些气泡互相嵌套并且整齐地排列成蜂窝 型,排列的结构是在写代码时定义的。但是,究竟是什么生成了一个新的气泡?只有函数会生成新的气泡吗? JavaScript 中的其 他结构能生成作用域气泡吗?函数中的作用域JavaScript 具有基于函数的作用域;无论标识符 声明出现在作用域中的何处,这个标识符所代表的变量或函数都将附属于所处作用域的气 泡。函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复 用(事实上在嵌套的作用域中也可以使用)。这种设计方案是非常有用的,能充分利用 JavaScript 变量可以根据需要改变值类型的“动态”特性。这是什么意思?隐藏的内部实现可以把变量和函数包裹在一个函数的作用域中,然后用这个作用域 来“隐藏”它们。Q: 为什么“隐藏”变量和函数是一个有用的技术?A: 大都是从最小特权原则中引申出来 的,也叫最小授权或最小暴露原则。这个原则是指在软件设计中,应该最小限度地暴露必 要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的 API 设计。设计上将具体内容私有化了,设计良好的软件都会 依此进行实现。规避冲突全局命名空间通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象。这个对象 被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属 性,而不是将自己的标识符暴漏在顶级的词法作用域中。模块管理任何库都无需将标识符加入到全局作用域中,而是通过依赖管理器 的机制将库的标识符显式地导入到另外一个特定的作用域中。避免同名标识符之间的冲突函数作用域在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。这种技术可以解决一些问题,但是它并不理想,因为会导致一些额外的问题:必须声明一个具名函数 foo(),意味着 foo 这个名称本身“污染”了所在作用域(在这个 例子中是全局作用域)必须显式地通过函数名(foo())调用这个函数才能运行其 中的代码。如果函数不需要函数名(或者至少函数名可以不污染所在作用域),并且能够自动运行, 这将会更加理想。(function foo(){ // <– 添加这一行 var a = 3; console.log( a ); // 3})(); // <– 以及这一行console.log( a ); // 2函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处。注意:区分函数声明和表达式最简单的方法是看 function 关键字出现在声明中的位 置(不仅仅是一行代码,而是整个声明中的位置)。如果 function 是声明中 的第一个词,那么就是一个函数声明,否则就是一个函数表达式。片段中 foo 被绑定在函数表达式自身的函数中而不是所在作用域中。类似的还有于 +function foo() {}() 对函数求值的操作,都能做到避免泄露换句话说,(function foo(){ .. })作为函数表达式意味着foo只能在..所代表的位置中 被访问,外部作用域则不行。foo 变量名被隐藏在自身中意味着不会非必要地污染外部作 用域。匿名和具名setTimeout( function() { console.log(“I waited 1 second!”);}, 1000 );这叫做匿名函数表达式, 因为function()没有名称标识符。函数表达式可以是匿名的,而函数声明则不可以省略函数名.匿名函数表达式写起来简单快捷,但是它有几个缺点需要考虑:匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。如果没有函数名,当函数需要引用自身时,只能使用已经过期的arguments.callee引用,比如在递归中。另一个函数需要引用自身的例子是在事件触发后事件监听器需要解绑自身。匿名函数省略了对于代码可读性/可理解性很重要的函数名。一个描述性的名词可以让代码不言自明。行内函数表达式非常强大且有用——匿名和具名之间的区别并会有对这点有任何影响。 给函数表达式指定一个函数名可以有效的解决以上问题。 始终给函数表达式命名是一个最佳实践。setTimeout( function timeoutHandler() { // <– 快看,我有名字了! console.log( “I waited 1 second!” );}, 1000 );立即执行函数表达式几年前社区给它规定了一个术语:IIFE,代表立即执行函数表达式 (Immediately Invoked Function Expression);IIFE的形式有下面俩种:(function(){ .. })()(function(){ .. }())用法1, 把它们当作函数调用并传递参数进去例如:var a = 2;(function IIFE( global ) { var a = 3; console.log( a ); // 3 console.log( global.a ); // 2})( window );console.log( a ); // 2我们将 window 对象的引用传递进去,但将参数命名为 global,因此在代码风格上对全局 对象的引用变得比引用一个没有“全局”字样的变量更加清晰。当然可以从外部作用域传 递任何你需要的东西,并将变量命名为任何你觉得合适的名字。这对于改进代码风格是非 常有帮助的。用法2,解决 undefined 标识符的默认值被错误覆盖导致的异常(虽 然不常见)。例如:将一个参数命名为 undefined,但是在对应的位置不传入任何值,这样就可以 保证在代码块中 undefined 标识符的值真的是 undefined:undefined = true; // 给其他代码挖了一个大坑!绝对不要这样做!(function IIFE( undefined ) { var a; if (a === undefined) {console.log( “Undefined is safe here!” );}})();用法3:倒置代码的运行顺序例如:将需要运行的函数放在第二位,在 IIFE 执行之后当作参数传递进去。这种模式在 UMD(Universal Module Definition)项目中被广 泛使用。尽管这种模式略显冗长,但有些人认为它更易理解。var a = 2;(function IIFE( def ) { def( window );})(function def( global ) { var a = 3; console.log( a ); // 3 console.log( global.a ); // 2});块作用域块作用域的用处:变量的声明应该距离使用的地方越近越好,并最大限度地本地化。块作用域是一个用来对之前的最小授权原则进行扩展的工具,将代码从在函数中隐藏信息 扩展为在块中隐藏信息。为什么要把一个只在 for 循环内部使用(至少是应该只在内部使用)的变量 i 污染到整个函数作用域中呢?可惜,表面上看 JavaScript 并没有块作用域的相关功能。withwith 关键字。它不仅是一个难于理解的结构,同时也是块作用域的一 个例子(块作用域的一种形式),用 with 从对象中创建出的作用域仅在 with 声明中而非外 部作用域中有效。try/catch非常少有人会注意到 JavaScript 的 ES3 规范中规定 try/catch 的 catch 分句会创建一个块作用域,其中声明的变量仅在 catch 内部有效。例如: try { undefined(); // 执行一个非法操作来强制制造一个异常 } catch (err) { console.log( err ); // 能够正常执行! } console.log( err ); // ReferenceError: err not found尽管这个行为已经被标准化,并且被大部分的标准 JavaScript 环境(除了老 版本的 IE 浏览器)所支持,但是当同一个作用域中的两个或多个 catch 分句 用同样的标识符名称声明错误变量时,很多静态检查工具还是会发出警告。 实际上这并不是重复定义,因为所有变量都被安全地限制在块作用域内部, 但是静态检查工具还是会很烦人地发出警告。为了避免这个不必要的警告,很多开发者会将 catch 的参数命名为 err1、 err2 等。也有开发者干脆关闭了静态检查工具对重复变量名的检查。letES6 改变了现状,引入了新的 let 关键字,提供了除 var 以外的另一种变量声明方式。 var foo = true; if (foo) { let bar = foo * 2; bar = something( bar ); console.log( bar ); } console.log( bar ); // ReferenceErrorES6中的if表达式中的{}并不具备块级作用域的划分,仅仅只能表明一个语句块,因为要在其中声明块级作用域变量还需要let来辅助。let 关键字可以将变量绑定到所在的任意作用域中(通常是 { .. } 内部)。换句话说,let为其声明的变量隐式地了所在的块作用域。在开发和修改代码的过 程中,如果没有密切关注哪些块作用域中有绑定的变量,并且习惯性地移动这些块或者将 其包含在其他的块中,就会导致代码变得混乱。为块作用域显式地创建块可以部分解决这个问题,使变量的附属关系变得更加清晰。通常 来讲,显式的代码优于隐式或一些精巧但不清晰的代码。显式的块作用域风格非常容易书 写,并且和其他语言中块作用域的工作原理一致:var foo = true;if (foo) { { // <– 显式的快 let bar = foo * 2; bar = something( bar ); console.log( bar ); }}console.log( bar ); // ReferenceError只要声明是有效的,在声明中的任意位置都可以使用 { .. } 括号来为 let 创建一个用于绑 定的块。在这个例子中,我们在 if 声明内部显式地创建了一个块,如果需要对其进行重 构,整个块都可以被方便地移动而不会对外部 if 声明的位置和语义产生任何影响。垃圾收集另一个块作用域非常有用的原因和闭包及回收内存垃圾的回收机制相关。function process(data) {// 在这里做点有趣的事情}var someReallyBigData = { .. };process( someReallyBigData );var btn = document.getElementById( “my_button” );btn.addEventListener( “click”, function click(evt) { console.log(“button clicked”);}, /capturingPhase=/false );click 函数的点击回调并不需要 someReallyBigData 变量。理论上这意味着当 process(..) 执 行后,在内存中占用大量空间的数据结构就可以被垃圾回收了。但是,由于 click 函数形成 了一个覆盖整个作用域的闭包,JavaScript 引擎极有可能依然保存着这个结构(取决于具体 实现)。块作用域可以打消这种顾虑,可以让引擎清楚地知道没有必要继续保存 someReallyBigData 了:function process(data) {// 在这里做点有趣的事情}// 在这个块中定义的内容可以销毁了!{ let someReallyBigData = { .. }; process( someReallyBigData );}var btn = document.getElementById( “my_button” );btn.addEventListener( “click”, function click(evt){ console.log(“button clicked”);}, /capturingPhase=/false );为变量显式声明块作用域,并对变量进行本地绑定是非常有用的工具,可以把它添加到你的代码工具箱中了。let循环for 循环头部的 let 不仅将 i 绑定到了 for 循环的块中,事实上它将其重新绑定到了循环 的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。每个迭代进行重新绑定的原因非常有趣,我们会在第 5 章讨论闭包时进行说明。const除了 let 以外,ES6 还引入了 const,同样可以用来创建块作用域变量,但其值是固定的 (常量)。之后任何试图修改值的操作都会引起错误。小结函数是 JavaScript 中最常见的作用域单元。本质上,声明在一个函数内部的变量或函数会在所处的作用域中“隐藏”起来,这是有意为之的良好软件的设计原则。 但函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指 { .. } 内部)。从 ES3 开始,try/catch 结构在 catch 分句中具有块作用域。在 ES6 中引入了 let 关键字(var 关键字的表亲),用来在任意代码块中声明变量。if (..) { let a = 2; } 会声明一个劫持了 if 的 { .. } 块的变量,并且将变量添加到这个块 中。 有些人认为块作用域不应该完全作为函数作用域的替代方案。两种功能应该同时存在,开 发者可以并且也应该根据需要选择使用何种作用域,创造可读、可维护的优良代码。 ...

January 30, 2019 · 3 min · jiezi

重读你不知道的JS (上) 第一节二章

你不知道的JS(上卷)笔记你不知道的 JavaScriptJavaScript 既是一门充满吸引力、简单易用的语言,又是一门具有许多复杂微妙技术的语言,即使是经验丰富的 JavaScript 开发者,如果没有认真学习的话也无法真正理解它们.上卷包括俩节:作用域和闭包this 和对象原型作用域和闭包希望 Kyle 对 JavaScript 工作原理每一个细节的批判性思 考会渗透到你的思考过程和日常工作中。知其然,也要知其所以然。词法作用域作用域共有俩种主要的工作模型: 词法作用域和动态作用域。词法阶段词法化:大部分标准语言编译器的第一个工作阶段叫作词法化(也叫单词化)。词法化的过程会对源代码中的字符进行检查,如果是有状态的解析过程,还会赋 予单词语义。词法作用域定义在词法阶段的作用域由你在写代码时将变量和块作用域写在哪来决定的,因此当词法分析器处理代码时会保持作用域不变。欺骗词法作用域: 在词法分析器处理过后依然可以修改作用域。事实上,让词法作用域根据词法关系保持书写时的自然关系不变是一个非常好的最佳实践。“作用域气泡法” 划分作用域查找作用域气泡的结构和互相之间的位置关系给引擎提供了足够的位置信息,引擎用这些信息来查找标识符的位置。作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者向上进行。作用域查找会在找到第一个匹配的标识符时停止,或者直至找到最后一个全局作用域处。window.a的方式可以访问那些被同名变量遮蔽了的全局变量,但非全局变量如果被遮蔽,就无法访问到了遮蔽效应: 在多层的嵌套作用域中可以定义同名的标识符。欺骗词法俩种欺骗手段:eval和with;社区认为使用这俩种机制并不是什么好主意,因为使用这俩种机制会导致性能下降另外一个不推荐使用 eval(..) 和 with 的原因是会被严格模式所影响(限 制)。with 被完全禁止,而在保留核心功能的前提下,间接或非安全地使用 eval(..) 也被禁止了。evalJavaScript 中的 eval(..) 函数可以接受一个字符串为参数,并将其中的内容视为好像在书 写时就存在于程序中这个位置的代码。换句话说,可以在你写的代码中用程序生成代码并 运行,就好像代码是写在那个位置的一样。function foo(str, a) { eval( str ); // 欺骗! console.log( a, b );}var b = 2;foo( “var b = 3;”, 1 ); // 1, 3eval通常被用来执行动态创建的代码在这个例子中,为了展示的方便和简洁,我们传递进去的“代码”字符串是 固定不变的。而在实际情况中,可以非常容易地根据程序逻辑动态地将字符 拼接在一起之后再传递进去。eval(..) 通常被用来执行动态创建的代码,因 为像例子中这样动态地执行一段固定字符所组成的代码,并没有比直接将代 码写在那里更有好处。在严格模式的程序中,eval(..) 在运行时有其自己的词法作用域,意味着其 中的声明无法修改所在的作用域。类似:setTimeout的第一个参数为字符串时;new Function的最后一个字符串参数;等都不提倡,不要使用。withwith 通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象 本身。例如:var obj = { a: 1, b: 2, c: 3 };// 单调乏味的重复 “obj” obj.a = 2;obj.b = 3;obj.c = 4;// 简单的快捷方式with (obj) { a = 3; b = 4; c = 5;}// 但实际上这不仅仅是为了方便地访问对象属性。考虑如下代码:function foo(obj) { with (obj) { a = 2; }}var o1 = { a: 3 };var o2 = { b: 3 };foo( o1 );console.log( o1.a ); // 2foo( o2 );console.log( o2.a ); // undefinedconsole.log( a ); // 2——不好,a 被泄漏到全局作用域上了!可以注意到一个奇怪的副作用,实际上 a = 2 赋值操作创建了一个全局的变量 a。这 是怎么回事?with 可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对 象的属性也会被处理为定义在这个作用域中的词法标识符。尽管 with 块可以将一个对象处理为词法作用域,但是这个块内部正常的 var 声明并不会被限制在这个块的作用域中,而是被添加到 with 所处的函数作 用域中。with 这种将对象及其属性放进一个作用域并同时分配标识符的行为很让人费解。性能JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的 词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到 标识符。小结词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段 基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它 们进行查找。JavaScript 中有两个机制可以“欺骗”词法作用域:eval(..) 和 with。前者可以对一段包 含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在 运行时)。后者本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作 用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认 为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们。 ...

January 29, 2019 · 1 min · jiezi

重读你不知道的JS(上) 第一节一章

你不知道的JS(上卷)笔记你不知道的 JavaScriptJavaScript 既是一门充满吸引力、简单易用的语言,又是一门具有许多复杂微妙技术的语言,即使是经验丰富的 JavaScript 开发者,如果没有认真学习的话也无法真正理解它们.上卷包括俩节:作用域和闭包this 和对象原型作用域和闭包希望 Kyle 对 JavaScript 工作原理每一个细节的批判性思 考会渗透到你的思考过程和日常工作中。知其然,也要知其所以然。作用域是什么?俩个事实能够储存变量值并能在之后对这个值进行访问和修改(几乎所有编程语言最基本的功能之一)若没有了状态这个概念,程序虽然也能够执行一些简单的任务,但它会受到高度限制,做 不到非常有趣。(正是这种储存和访问变量的值的能力将状态带给了程序)提出问题变量存在哪?程序需要时如何找到他们?需要一套规则来处理变量的问题,解决上述问题需要一套设计良好的规则来存储变量,并且之后可以方便的找到变量,这套规则被称为 作用域。新的问题作用域规则在哪里?怎么样设置这些规则?编译原理JavaScript是一门编译语言。不是提前编译编译结果也不能在分布式系统中移植传统语言的编译流程:分词/词法分析(Tokenizing/Lexing)这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代 码块被称为词法单元(token)。例如,考虑程序var a = 2;。这段程序通常会被分解成 为下面这些词法单元:var、a、=、2 、;。空格是否会被当作词法单元,取决于空格在 这门语言中是否具有意义。。分词(tokenizing)和词法分析(Lexing)之间的区别主要差异在于词法单元的识别是通过有状态还是无状态的方式进行的。简 单来说,如果词法单元生成器在判断 a 是一个独立的词法单元还是其他词法单元的一部分时,调用的是有状态的解析规则,那么这个过程就被称为词法分析。解析/语法分析(Parsing)这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法 结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。var a = 2; 的抽象语法树中可能会有一个叫作 VariableDeclaration 的顶级节点,接下 来是一个叫作 Identifier(它的值是 a)的子节点,以及一个叫作 AssignmentExpression 的子节点。AssignmentExpression 节点有一个叫作 NumericLiteral(它的值是 2)的子节点。代码生成将 AST 转换为可执行代码的过程称被称为代码生成。这个过程与语言、目标平台等息 息相关。抛开具体细节,简单来说就是有某种方法可以将 var a = 2; 的 AST 转化为一组机器指 令,用来创建一个叫作 a 的变量(包括分配内存等),并将一个值储存在 a 中。JavaScript 引擎不会有大量的(像其他语言编译器那么多的)时间用来进行优化,因 为与其他语言不同,JavaScript 的编译过程不是发生在构建之前的。任何 JavaScript 代码片段在执行前都要进行编译(通常就在执行前)。因此, JavaScript 编译器首先会对 var a = 2; 这段程序进行编译,然后做好执行它的准备,并且 通常马上就会执行它。理解作用域对话形式模拟作用域的工作方式演员表参与到对程序 var a = 2; 进行处理的过程中的演员们引擎 从头到尾负责整个 JavaScript 程序的编译及执行过程。编译器 负责语法分析及代码生成等。作用域 负责收集并维护由所有声明的标识符(变量)组成的一系列查 询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。对话编译器首先会将这段程序分解成词法单元,然后将词法单元解析成一个树结构。但是当编 译器开始进行代码生成时,它对这段程序的处理方式会和预期的有所不同。可以合理地假设编译器所产生的代码能够用下面的伪代码进行概括:“为一个变量分配内 存,将其命名为 a,然后将值 2 保存进这个变量。”然而,这并不完全正确。事实上编译器会进行如下处理。遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的 集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作 用域的集合中声明一个新的变量,并命名为 a。接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值 操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a 的 变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量(查看 1.3 节)。总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如 果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对 它赋值。编译器中的操作LHSRHS异常为什么区分 LHS 和 RHS 是一件重要的事情?因为在变量还没有声明(在任何作用域中都无法找到该变量)的情况下,这两种查询的行 为是不一样的。如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常。值得注意的是,ReferenceError 是非常重要的异常类型。相较之下,当引擎执行 LHS 查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非 “严格模式”下。严格模式下,未声明的RHS和LHS俩者行为相同,都会是 ReferenceError。ReferenceError 同作用域判别失败相关,而 TypeError 则代表作用域判别成功了,但是对 结果的操作是非法或不合理的。 ...

January 29, 2019 · 1 min · jiezi