对JavaScript秘密花园的完全解读
花了将近一个下午的时间来读秘密花园,对基础总结的很好。在过了一遍之后,感觉不满意,于是以记笔记和扩展的方式再来一遍,希望将js的基础夯结实,以便在日后往高级进阶的路上少栽跟头。
JavaScript秘密花园:传送门
1.js中所有变量可当对象使用,除null和defined外。
这里的疑问是所有变量指什么,如何当对象用,作为对象有何特性?
a.对象上的内置属性和方法可以随意使用。
b.对象上可以任意添加属性和方法。
c.对象是可以从原型链上继承属性和方法的。
d.能应用到对象上的方法也可以应用到此处所指的变量上。
f.可以作为参数被传递。
g.可以作为原型被赋值给构造函数的prototype属性。
一言以蔽之,js中一切皆对象,那任何场景都可以使用变量替代。
作者所言所有变量,无论是赋值为原始值还是引用值的皆为变量,甚至未赋值的或者赋值为空对象的,赋值为NaN的,Infinity等也为变量。不过在举例的时候,作者举了false,后面还举例了数字原始值,所以这里所谓变量表述不很准确。可以代之以所有变量和值。
这就将包括所有原始值,以及引用值变量或者直接的对象代码段、数组代码段、函数代码段,不过因为可能解析错误的问题,后者用()分隔符进行分组后,同样可以当对象使用。
ok,而作为对象使用,以上a-g,可算对象的不完全属性,很显然上面的描述并不能适用对象所有特性。只是他们可以使用内置属性和方法,b,c,d不适用。而f,g当然是可以的。所以个人觉得更准确的表述是可以在所有变量和值上应用对象所具有的内置属性和方法。
2.一个常见的误解是数字的字面值(literal)不能当作对象使用。这是因为 JavaScript 解析器的一个错误, 它试图将点操作符解析为浮点数字面值的一部分。
这种解析错误,不单单是数字,还有{}对象,函数体,new实例化代码串(如new Date.toString)等。解决办法就是给他们加上分组符“()”。而数字的选择更多,如2..toString(),2 .toString(),(2).toString()。等。
3.对象作为数据类型
对象作为一种数据类型,正如上周研究的枚举类型,数组类型,函数类型,是一种哈希表,散列表。也就是键值对。
4.对象的访问
.操作符访问和中括号操作符访问。.只针对作为标识符的键,这个键本身是确定的不变的,值可以任意变。而中括号可以访问包括原始值,标识符及组合键作为键的所有属性方法。
a.原始值,o[0],o[1],这种访问用.号无法访问。
b.字符串,o['a b'],即键中带有空格,js保留字等.号访问无法正确解析的情况。
c.组合键无法读取,比如o[a + "_"],a是个变量,.号无法访问。
最主要还是c情况比较常见。a情况也是存在的。
5.删除属性的唯一方法是使用 delete 操作符;设置属性为 undefined 或者 null 并不能真正的删除属性, 而仅仅是移除了属性和值的关联。
这句话的意思是设置属性为undefined或者null只是将分配的那个内存块置为这两个内置原始值,内存块在。前面分配的值被取代了。作者所讲仅仅是移除关联,我不太赞同,作者的话类似于引用值类型,移除了属性跟堆内存引用类型的关联,是通过重置栈内存中的引用类型值(地址值)做到的。而delete则是将属性名跟值一起删掉了。因而再去查找就找不到了。前者属性能找到,只不过值已经被重置了。
6.对象的属性名可以使用字符串或者普通字符声明。关键字或者保留字作为属性名在ES5前报语法错误,为了向前兼容,使用字符串字面量。最最好的就是避免使用关键字和保留字了。非要用到就用字符串来代替,这是个很好的编码习惯。
7.原型链举例:
function Foo() { this.value = 42; } Foo.prototype = { method: function() {} }; function Bar() {} // 设置Bar的prototype属性为Foo的实例对象 Bar.prototype = new Foo(); Bar.prototype.foo = 'Hello World'; // 修正Bar.prototype.constructor为Bar本身 Bar.prototype.constructor = Bar; var test = new Bar() // 创建Bar的一个新实例
以上这段代码,原型链是:
test【Bar实例】-> __proto__->foo属性 + new Foo()实例->value属性 + __proto__ -> method方法
8.当原型属性用来创建原型链时,可以把任何类型的值赋给它(prototype)。 然而将原子类型赋给 prototype 的操作将会被忽略。
这句话的后半句不全正确。原子类型赋值给自定义函数的prototype,会更改prototype的值,但是在实例化的时候它会直接从Object根对象去继承属性,从这个角度来说赋值操作被忽略了。
9.当使用 for in 循环遍历对象的属性时,原型链上的所有属性都将被访问。
这句话更准确的表述是可枚举类型的所有属性。
10.在写复杂的 JavaScript 应用之前,充分理解原型链继承的工作方式是每个 JavaScript 程序员必修的功课。 要提防原型链过长带来的性能问题,并知道如何通过缩短原型链来提高性能。 更进一步,绝对不要扩展内置类型的原型,除非是为了和新的 JavaScript 引擎兼容。
这句总结的提醒非常好,其一保证较短的原型链提高查找性能,其二不要扩展内置类型原型链,扩展的目的仅仅是兼容。
11.hasOwnProperty 是 JavaScript 中唯一一个处理属性但是不查找原型链的函数。
面临的问题有js中有哪些处理属性的函数?其二有哪些查找原型链的函数或语法?
for...in 和in,keys,values,getOwnPropertyNames等。当然还有很多其他比如数组,或者函数Function中有的对属性操作的函数,这里不罗列。
12.hasOwnProperty不会被保护,因此要使用原始的hasOwnProperty可以使用call,apply。当检查对象上某个属性是否存在时,hasOwnProperty 是唯一可用的方法。 同时在使用 for in loop 遍历对象时,推荐总是使用 hasOwnProperty 方法, 这将会避免原型对象扩展带来的干扰。
13.不使用 hasOwnProperty,则这段代码在原生对象原型(比如 Object.prototype)被扩展时可能会出错。代码如下:
for(var i in foo) { if (foo.hasOwnProperty(i)) { console.log(i); } }
一个广泛使用的类库 Prototype 就扩展了原生的 JavaScript 对象。 因此,当这个类库被包含在页面中时,不使用 hasOwnProperty 过滤的 for in 循环难免会出问题。
作者在此处指出的对于扩展了原生对象原型而生成的对象不使用hasOwnProperty可能会出错。以及后面所说难免出问题。个人觉得表述不清,应该说是不能得到预想结果比较好。也就是会将扩展的属性或方法遍历出来,这往往不是程序员想要的。
因此作者提醒:推荐总是使用 hasOwnProperty。不要对代码运行的环境做任何假设,不要假设原生对象是否已经被扩展了。
14. 函数一个常见的用法是把匿名函数作为回调函数传递到异步函数中。
这里回调函数指的就是作为参数传递的函数。异步函数在js中表现为ajax异步,定时器异步以及事件触发函数异步。这里的理解不够准确和彻底,需要更深一步,暂时以阮一峰4种异步编程为准理解。传送门:http://blog.csdn.net/sdfujichao/article/details/52160028
15.函数声明,函数变量赋值匿名函数,函数变量赋值命名函数三种情况各变量名的可见度情况分析:
函数声明:函数名在函数内外皆可见可读。因此才能使内部以名称调用自己成为可能。
var foo = function bar() { bar(); // 正常运行 } bar();
bar 函数声明外是不可见的,这是因为我们已经把函数赋值给了 foo; 然而在 bar 内部依然可见。这是由于 JavaScript 的 命名处理所致, 函数名在函数内总是可见的。
function f(f){ var f = 0; function f(){f.value = 1;} console.log(f); }
通过以上代码的测试,普通变量的赋值最有优先级被打印出,其次是内部函数声明,再其次是形参,最后是外部函数名。符号表示:内部私有变量 > 函数声明 > 形参变量 > 外部函数名。
注意:在IE8及IE8以下版本浏览器bar在外部也是可见的,是因为浏览器对命名函数赋值表达式进行了错误的解析, 解析成两个函数 foo 和 bar
16.this存在的位置:
全局对象中,this即是全局。函数直接调用中,会在函数预解析时创建this对象,此时判断其所有者是谁,this赋值谁。作为方法的函数this是他的所有者,当然当该方法被赋值给其他变量,this会发生改变。构造函数this是返回的那个新对象,可能是new构造出来的,也可能是函数返回的对象。而call和apply会显式的改变this指向。bind则是新复制一个被bind的函数,然后绑定到bind第一个参数上去,老函数的this并未发生转向。
简言之this一直被使用在与函数关联的地方。
所以在其他非函数的对象中出现this的话,因为无作用域的限制,都会指向全局。
第二个规则,也就是直接调用函数时,this 指向全局对象, 被认为是JavaScript语言另一个错误设计的地方,因为它从来就没有实际的用途。个人认为不见得就是错误,这里的this在严格模式下是undefined,只有非严格模式下才指向全局。this被预先定义的好处在于它显式指定的时候可以直接使用。免得在显式指定的时候才做定义操作。
17.对以下函数改变内层this指向的几种办法:
Foo.method = function() { var that = this; function test() { // 使用 that 来指向 Foo 对象 } test(); }
a.that缓存;b.call/apply;c.bind.
Foo.method = function() { function test() { // 使用 that 来指向 Foo 对象 } test.call(this); //test.apply(this); } Foo.method = function() { function test() { // 使用 that 来指向 Foo 对象 } test = test.bind(this); }
18.this的丢失及晚绑定特性。
虽然 this 的晚绑定特性似乎并不友好,但这确实是基于原型继承赖以生存的土壤。
this的丢失是指在原来绑定的this后重新赋值,this会发生转向,这也正体现了this的灵活机动性。尤其方法赋值给另一个方法或者变量的时候。
var f = obj.method;
很显然method的this在被赋值后,其所属权已经交给了f所属的对象,这里是全局。
而this的晚绑定特性是原型继承赖以生存的土壤,先找找二者之间的必然联系。this是在函数初始化或者预编译的时候生成的,它指向它的所有者对象,也即上面提到的五种情况。在函数中显式定义时使用的this指向所有者对象。当构造实例的时候this转向到新的实例或者返回对象。构造的实例的原型是构造函数的prototype或者返回对象原型,也或者是被自定义的原型对象。this在构造函数和实例对象之间架起了一座桥梁。让定义在构造函数中的公共属性和方法能够实例化到众多的实例对象中去。这应该就是所谓生存土壤说法的来源。而且this的存在保证了公共变量和方法的各自独立性,不会像原型链上的方法和属性进行共享。而且原型是可以读写公共属性方法的。
19.JavaScript 中不可以对作用域进行引用或赋值,因此没有办法在外部访问 count 变量。 唯一的途径就是通过那两个闭包。
这句话的表述有待理清。首先js不可以对作用域进行引用或赋值。所谓的作用域指的是这个函数限定的变量域,仅指私有变量,也就是var声明的变量和函数声明的变量或形参变量。这些变量在外部是无法引用或直接赋值的,因为不可见,不可读,不可写。内部和外部没有联系。
20.setTimeout和setInterval异步调用。
它们由于是浏览器另外的线程对js引擎的插入,因此他们的执行在扔出计时器的原代码,在所有js代码队列(先进先出)中的代码之后。
通过以下代码验证,见得事件和定时器都放在正常代码之后执行的。
var s = new Date; var arr = []; var arr1 = [],arr2 = [],arr3 = [],arr4 = [],arr5 = [], arr6 = []; var a = 10; for(var i = 0; i < 1000000; i++){ arr.push(i); setTimeout(function(){ a > 0 ? console.log(a--) : -1; },5000); }; for(var i = 0; i < 200000; i++){ arr1.unshift(i); }; var ele = document.getElementById("codeTest"); ele.addEventListener("mouseover",function(){this.style.color = "rgb(255, 165, 0)"}); var t = new Date; t - s;
在后面还需要对setTimeout和setInterval进行深入探究。
21.注意: 由于 arguments 已经被定义为函数内的一个变量。 因此通过 var 关键字定义 arguments 或者将 arguments 声明为一个形式参数, 都将导致原生的 arguments 不会被创建。
经过测试,的确如此。
22.将arguments转化为真正数组。
方法一:Array.prototype.slice.call(arguments);
性能不佳。
方法二:利用apply可以传入数组参数的特性,直接调用函数。
function foo() { bar.apply(null, arguments); } function bar(a, b, c) { console.log(a + b + c); }
bar被调用的时候,虽然传入的是类数组、数组参数,但在计算的时候该数组在apply调用的内部被拆分成了字符串,因此达到了不专门转换数组而行了转换之实。
23.解绑定包装器。
function Foo() {} Foo.prototype.method = function(a, b, c) { console.log(this, a, b, c); }; // 创建一个解绑定的 "method" // 输入参数为: this, arg1, arg2...argN Foo.method = function() { // 结果: Foo.prototype.method.call(this, arg1, arg2... argN) Function.call.apply(Foo.prototype.method, arguments); };
解析这个包装器,Foo.prototype.method这个函数对象上绑定了Function上的call方法,arguments被传入到method的call方法上执行。前一步进行了arguments的数组转字符串化,后一步则执行。arguments字符串化后,第一个应该是指定的绑定对象,从第二个开始是传入的其他参数。
相当于以下代码就好理解了:
Foo.method = function() { var args = Array.prototype.slice.call(arguments); Foo.prototype.method.apply(args[0], args.slice(1)); };
24.arguments 对象为其内部属性以及函数形式参数创建 getter 和 setter 方法。因此,改变形参的值会影响到 arguments 对象的值,反之亦然。
以下为模拟代码:
function f(a,b,c){ var arguments = arguments; var len = f.length; var arr = []; for(var j = 0; j < len; j++){ arr.push(f[j]); } for(var i in arguments){ (i).getter = function(){ return arguments[i]; }; (i).setter = function(){ if(f.hasOwnProperty(i)) arguments[i] = f[i]; } } console.dir(arguments); }
以下这段代码在严格模式和非严格模式下结果不一样:
// 阐述在 ES5 的严格模式下 `arguments` 的特性 function f(a) { "use strict"; a = 42; return [a, arguments[0]]; } var pair = f(17); // 阐述在 ES5 的严格模式下 `arguments` 的特性 function f(a) { //"use strict"; a = 42; return [a, arguments[0]]; } var pair = f(17);
前者输出42,17,后者输出42,42。的确是形参跟arguments对应元素发生了关联。作者意思用setter和getter进行了同步,不太好验证。也就是说在非严格模式下形参及arguments的元素都是存取器类型。当一个发生改变的时候,都会触发存的操作,将对应改变的值设置为当前的形参或者arguments元素的值。至于是不是arguments对象做的这些内部工作就不得而知了。暂且相信作者所说。
25.内联特性。
function foo() {
arguments.callee; // do something with this function object
arguments.callee.caller; // and the calling function object
}
function bigLoop() {
for(var i = 0; i < 100000; i++) {
foo(); // Would normally be inlined...
}
}
上面代码中,foo 不再是一个单纯的内联函数 inlining(译者注:这里指的是解析器可以做内联处理), 因为它需要知道它自己和它的调用者。 这不仅抵消了内联函数带来的性能提升,而且破坏了封装,因此现在函数可能要依赖于特定的上下文。因此强烈建议大家不要使用 arguments.callee 和它的属性。
内联处理,应该是指callee,及caller被解析后进行指向的一个内部操作。上面代码应该插入了for循环,callee好找,caller可能就要受到for的影响了。因为caller是在调用中发生连接的,应该是。作者所谓的性能提升可能就因为这个查找过程被抵消。破坏封装应该是指单纯的从arguments对象到自己,到调用者之间的完整性。插入for破坏了这种完整性。依赖特定上下文也就容易理解了。建议不使用。那匿名函数中如何完成对自己的递归调用呢?所以这里应该是指非必要使用callee的情况罢!!
26.我们常听到的一条忠告是不要使用 new 关键字来调用函数,因为如果忘记使用它就会导致错误。
使用new关键字严格的来说不是调用函数,而是构造对象。所以作者在这里表述不清,到底是不建议使用new来调用函数还是构造对象。假如说是单单调用函数,犯不着加个new。如果是构造对象,当然构造方式有不少,new只是最基本的。忘记它导致错误(如果是弹出error),除非构造函数内部有机制阻止new,而这出现的几率不高。那剩下的就是说new和不new对于构造函数结果不一样咯。除非是专门的弥合了二者区别的工厂函数,的确new跟不new结果是不一样的。比如js的内置函数Number,new创建一个数字对象,而不new代表的是类型转化。有区别。暂且这么理解吧!
后面一句话说了:放弃原型链仅仅是因为防止遗漏 new 带来的问题,这似乎和语言本身的思想相违背。是不是可以验证以上说法呢!
27.虽然遗漏 new 关键字可能会导致问题,但这并不是放弃使用原型链的借口。 最终使用哪种方式取决于应用程序的需求,选择一种代码书写风格并坚持下去才是最重要的。
两点:放弃原型链有悖语言设计初衷;代码风格不拘泥,重要的是一贯。
28.注意: 如果不是在赋值语句中,而是在 return 表达式或者函数参数中,{...} 将会作为代码段解析, 而不是作为对象的字面语法解析。如果考虑到 自动分号插入,这可能会导致一些不易察觉的错误。
这句话比较难理解,赋值语句自不必说。主要是return 表达式,不加分号是个表达式。在这个表达式中不作为对象的字面量解析。首先作为对象的字面量它必须符合键值对规则,必须,号隔开等。但是测试过程中,作为return返回或者赋值语法一样,不符合对象语法仍旧弹error,所以我怀疑他这里说的不是作为对象字面语法解析的说法。按他说法返回[]就不是数组字面量,返回function(){}就不是函数字面量?当然{}在js中的确在不少地方作为代码段的分隔符,比如if。所以他说作为代码段解析也未尝不可,但理由呢?作不作为代码段跟这行后被插入分号有关联吗?用函数折行返回测试,同样报错。返回原始值依旧是,不过原始值被忽略,“;”号被直接插入到return之后。而如果作为代码段解析,那我在{}中放入非对象语法,也即如if一样,任意放入语句都不会出错的,但是没有,依旧出错,因此个人认为这里还是对象字面量。
再说函数参数,只能是作为实参传入的以{}分割的参数,本质来说它就是一个值,传进来的时候被赋值给形参。仍旧回到前面的赋值操作,这不是字面量又是啥?所以个人觉得作者的结论错误。
29.
// 脚本 A foo = '42'; // 脚本 B var foo = '42'
作者说这两段代码不同,那要看使用在哪儿,如果在全局作用域,都会给全局添加一个名为foo的属性。而若在函数作用域,当然就不一样了。所以表述不清,实在误导人。
30.JavaScript 中局部变量只可能通过两种方式声明,一个是作为函数参数,另一个是通过 var 关键字声明。
这个说法也很不严谨。在函数内部,变量有this,arguments,形参,函数声明,及普通函数变量几种。
31.一些容易被迷惑的变量及函数提升的情况。
if (!SomeImportantThing) { var SomeImportantThing = {}; }
bar(); var bar = function() {}; var someValue = 42; test(); function test(data) { if (false) { goo = 1; } else { var goo = 2; } for(var i = 0; i < 100; i++) { var e = data[i]; } }
由于在js中无块级作用域,只有全局和函数,因此所有的变量都只被这两种作用域区隔。上升到这个理解层面就好理解变量在函数内部尽管包含在其他各种各样的非函数作用域当中,但它仍旧不受其约束先预解析后赋值。if,while,switch,case,do...while,for,for...in,{}。好,下面对js中的所有语句做一下变量声明的测试(非严格模式):
eval:
function f(){ console.log(1); c();eval('function c(){setTimeout(f(),1000)}'); }
经测试函数内部的eval,当变量以var声明,则作为其局部变量,否则泄露到全局,而其他变量只是局部变量。不过对于变量定义和函数声明来说,没有预编译或者函数声明hoisting。
with:
function f(obj){ console.log(obj.name); with(obj){ name = "江太公"; sex = "男"; } console.log(obj.name); }
var o = {name:"silcano",sex:"male"}
f(o);
输出结果是:silcano,江太公
也就是说with不能创建新变量或属性,只能在原有的基础上操作。因此也就不存在变量提升的说法。它只是构建了一个暂时的对象作用域而已。貌似说法不准确,再来一个测试例子:
function f(obj){ console.log(obj.name.a); with(obj){ name = {a:1,b:2}; sex = "男"; } console.log(obj.name.a); }
此时都输出:1,给name赋值了一个新对象,且创建了新变量,按理说第一次a是没有的,应该是undefined才对,但事实不是(经检测应该是浏览器之前添加的变量影响,实际第一个输出是undefined,如下:),

那就证明还是有预解析的情况存在,因为在f函数中,obj是形参变量,name是形参的内部变量,a是形参子元素的子元素变量,b也是,sex跟name同级,因为在函数内部除了function作用域对于外部函数不可见外其他包括with构建的临时作用域变量对其都是可见的。所以个人认为他也进行了变量提升。貌似还是不对。应该是with形成的作用域添加到了该作用域链的最顶端,那第一次查找a就在这个作用域中查找而非f函数的作用域。作用域的形成是在预解析阶段,这样就能解释的通了。不过当未添加新变量也就是前面一种情况,貌似又解释不通了,那我们看变量优先级。另外还有个问题,with中是可以定义var变量的。
a.with作用域及作用域链。
传送门:《javascript:with的用法以及延长作用域链》。
with的作用: 通俗的说,就是引用对象,并对该对象上的属性进行操作,其作用是可以省略重复书写该对象名称,起到简化书写的作用。with代码块中,javascript引擎对变量的处理方式是:先查找是不是该对象的属性,如果是,则停止。如果不是继续查找是不是局部变量。就算在with语句中使用 var 运算符重新定义变量(该变量是with引用对象的属性),如果该属性是可写属性,那么也会给对象的属性赋值。代码为证:
+function f(obj){ with(obj){ var name = "silcano"; } }(o = {name:1})
o
Object {name: "silcano"}
以上是代码及控制台输出结果。
如果你想通过with语句,对引用对象添加多个属性,并为每个属性赋值,这是不可能的!也就是说,要赋值的只能是对象已经存在并且可以写入的属性(不能是只读属性)。如下图:

with语句的变量对象是只读的,结果url就成了函数执行环境的一部分,因而可以作为函数的值被返回。也就是说with引用的对象尽管也有变量对象,但是只读,因此在with中出现的新变量会添加到函数变量对象上去,这就起到了延长作用域链的效果。“由于with语句块中作用域的‘变量对象’是只读的,所以在他本层定义的标识符,不能存储到本层,而是存储到它的上一层作用域。在Javascript的作用域中(作用域,想想就是函数块,每个函数都会有个函数名,就算是匿名函数也有个空函数名),那么创建作用域的时候,本层的标识符就可以寄托在这个作用域下,而with语句块中作用域的‘变量对象’是只读的,不能存储标识符,只能存储在其上一层,这就是延长作用域链。其实,这和上面说的不能给对象添加属性有同工之处。”
而且with中不用var进行限制,变量仍旧泄露到全局。如下:
console.log(function f(obj){ with(obj){ var name = "silcano"; age = "1"; } return age; }(o = {name:1}));
全局可访问age。也就是说在with中添加的变量依旧遵循function其他变量的定义规则,提升也会发生。如果是非var变量,则会提升到全局,但并不能提前访问。如图:

换成前面的对对象属性进行赋值,并创建新变量,也就是对象的对象的属性,如图:
console.log(function f(obj){ console.log(obj.name.x); with(obj){ var name = {x:1,y:2}; } //return age; }(o = {name:1}));
结果为undefiend,也就是说找不到x这个属性。去掉var改成如下:
console.log(function f(obj){ console.log(obj.name.m); with(obj){ name = {m:1,n:2}; } //return age; }(o = {name:1}));
依旧如故,找不到,在属性访问中显示undefined。
function f(obj){ console.log('p' in obj.name); with(obj){ name = {p:1,q:2}; sex = "男"; } console.log('p' in obj.name); } undefined f(c = {name:2}); VM19139:2 Uncaught TypeError: Cannot use 'in' operator to search for 'p' in 2(…)
通过以上测试可看得出在with引用对象的存在且可写属性上添加的变量是不能提升的。看来还是符合函数的var变量和函数声明的提升规则。由此便清楚了一大半。
b.with直接定义变量。这个问题在a中已回答,不赘述。变量是属性,则直接对属性操作,如果属性不存在,才在函数变量对象中添加这个变量,而且除了var和函数声明可以提升之外,其他都不能。这就包括无var的全局变量声明,在对象属性中添加的新变量。只有with引用对象是只读的,不能添加属性,但是将with引用忽略,直接对对象添加是可以的。所以这应该是with的一个设计特点。但是可以删除哦!但是可以对该属性赋值任意类型,包括在其中添加新变量。代码如下:

switch...case:
function f(e){ switch(e){ case 1: var a = 3; break; case 2: var a = 4; break; case 3: var a = 5; break; } return a; }
虽然代码如此,但实际定义只做了一次。后面两次定义都被忽略。函数声明后面声明覆盖前面声明,说不准变量定义也是如此呢!如下:
function f(e){ switch(e){ case 1: function a(){return 1;} break; case 2: function a(){return 2;} break; case 3: function a(){return 3;} break; } return a(); }
我们验证一下变量是不是后覆盖前,代码如下:
function f(e){ console.log(Object.prototype.toString.call(a)); switch(e){ case 1: var a = []; break; case 2: var a = {}; break; case 3: var a = function(){}; break; } }
在未赋值前是undefined类型。如下若提前输出函数:
function f(e){ console.log(a); switch(e){ case 1: function a(){return 1;} break; case 2: function a(){return 2;} break; case 3: function a(){return 3;} break; } }
显示都是未定义。所以可看出所谓的声明或变量定义提前只是说明有了这个标识符在,它的类型还是不确定的。符合js的语法规则。函数也是这样。
如此其他的语句笔者就不打算继续了。跟上面这几个我本身不太熟悉的语句一样。即便有各种条件判断又如何,它还是会提前。即便是提前返回如下,也是如此:
function f(){ console.log(a); return ; var a = 1; } f();
32.变量解析顺序:
- 当前作用域内是否有
var foo的定义。 - 函数形式参数是否有使用
foo名称的。 - 函数自身是否叫做
foo。 - 回溯到上一级作用域,然后从 #1 重新开始。
作者在此处写的并不完整,完整榜单是:
a.当前作用域内是否有var定义。
b.内部有否函数声明。
c.形式参数。
d.函数本身名字。
e.回溯上一级。
如图验证:


33.注意: 自定义 arguments 参数将会阻止原生的 arguments 对象的创建。
已验证。这个很好验证。
34.只有一个全局作用域导致的常见错误是命名冲突。在 JavaScript中,这可以通过 匿名包装器 轻松解决。
这个的意思我理解还是要使用var或函数声明,才能利用函数作用域起到避免命名冲突的效果。否则还是一样。比如全局有了一个名为foo的变量,我用闭包但是使用var和函数声明或者形参就可以避免与外部变量的冲突。
推荐使用匿名包装器(译者注:也就是自执行的匿名函数)来创建命名空间。这样不仅可以防止命名冲突, 而且有利于程序的模块化。
35.匿名函数被认为是 表达式;因此为了可调用性,它们首先会被执行。
关于表达式和语句的区别,我曾专门做过学习。不过要搞的一清二楚也是颇有难度的事情。简单来说:
表达式是原始表达式(this关键字、标识符引用、字面量引用、数组初始化、对象初始化和分组表达式)及原始表达式组合成的复杂表达式统称为表达式。语句可以加分号。这个后面还有必要深入一下。这里不多说了。
36.虽然在 JavaScript 中数组是对象,但是没有好的理由去使用 for in循环遍历数组。 相反,有一些好的理由不去使用 for in 遍历数组。
注意: JavaScript 中数组不是关联数组。 JavaScript 中只有对象来管理键值的对应关系。但是关联数组是保持顺序的,而对象不是。
由于 for in 循环会枚举原型链上的所有属性,唯一过滤这些属性的方式是使用 hasOwnProperty 函数, 因此会比普通的 for 循环慢上好多倍。
以上理由的确不错。for...in循环是自主循环,会按着js设定的规则去循环全部。而for是人工控制的,设定长度,依次遍历,不会上溯到原型链去。所以慢也就可以理解了。
置于关联数组是在其他语言中的语法规则,js不存在,用对象来保存键值对,但无顺着可言,尽管多数时间是按序读取的,但设计上并无此严格规则。
37.l = list.length 来缓存数组的长度。虽然 length 是数组的一个属性,但是在每次循环中访问它还是有性能开销。可能最新的 JavaScript 引擎在这点上做了优化,但是我们没法保证自己的代码是否运行在这些最近的引擎之上。实际上,不使用缓存数组长度的方式比缓存版本要慢很多。
这是一个很好的编码习惯,应该记住。
38.length 属性的 getter 方式会简单的返回数组的长度,而 setter 方式会截断数组。
这样看来所有的既能读又能写的属性很多可能都是存取器类型。尤其对于自动更新的来说,或者含有很多自动属性的属性来说。感性认识。昨天谈到了function中形参和arguments就是互为更新的,也就是自动化了同步更新,也是存取器属性的。
39.以下关于length被赋值的时候的代码:
var foo = [1, 2, 3, 4, 5, 6]; foo.length = 3; foo; // [1, 2, 3] foo.length = 6; foo; // [1, 2, 3]
译者注: 在 Firebug 中查看此时 foo 的值是: [1, 2, 3, undefined, undefined, undefined] 但是这个结果并不准确,如果你在 Chrome 的控制台查看 foo 的结果,你会发现是这样的: [1, 2, 3] 因为在 JavaScript 中 undefined 是一个变量,注意是变量不是关键字,因此上面两个结果的意义是完全不相同的。
两种浏览器的不同处理方式咱们暂且不提。主要看这样的处理合理性在哪儿。undefined在js中是作为一个变量而非关键字诸如true。

作为变量的undefined是允许赋值的。尽管设为了只读,不能改变值但也不报错。而作为关键字或保留字的true则报错。因此firebug显示的结果,实际3个undefined就是三个变量占位符。实际的长度应该还是6,所以他的处理方式不合理。即便是关键字貌似也没找到合理处。不过作为关键字是否可处理为不占有任何位置的一个标识符,不得而知。
undefined作为变量:
那截断后,之后到无穷取值都是undefined。而显然firebug就会产生混淆。删除的也是undefined,从未添加的也是,无法区分。而chrome则可以。删除的跟从未添加的回归到一样,都是undefined。好理解。
如果对length进行延长但不填充包括undefined在内的任何值的时候,使用for...in是遍历不到后面无值的元素的。只要赋值为undefined就可以遍历到它。这里也出现了区别。尽管从未赋值包括undefined和赋值了undefined字面量上显示的毫无二致,但前者遍历不到,后者可以。所以undefined跟undefined还有区别。


而且还有个区别,当手动设定了undefined值后,显示占位是单独的,其他是用*x的形式显示的。前者应该是在内存中开辟了空间,后者可能只是做了记录而已。另外使用undefined和'undefined'结果一致。不过赋值字符串的undefined也还有区别,如下:

而firebug的处理方式是 [1, 2, 3, undefined, undefined, undefined],按chrome来看就证明序号仍在,只是值变成了undefined,所以作者说二者完全不同了!![1,2,3]和[1,2,3,undefined,undefined,undefined]个人理解大概区别就在这里罢!!
不过在使用in去查firebug的时候还是显示无,那就是说序号也是被删除的了。不过他这种显示的确给人误解。不过至少可以证明那个位置上以前有过元素。
40.由于 Array 的构造函数在如何处理参数时有点模棱两可,因此总是推荐使用数组的字面语法 - [] - 来创建数组。
模棱两可表现在给Array传入的参数个数不一样的时候,值被按不同的功能使用。比如超过2个,则直接创建元素为2个的数组,并将此2者作为数组的元素1和2。而若只给定一个数字的时候,这个数字不是被用于创建一个元素的数组,而是作为数组的长度使用,这样就产生了歧义。也难怪作者更建议使用数组字面量来创建数组了。实际的编码中的确后者更常被使用,写入的代码更少自然效率更高。
41.这种优先于设置数组长度属性的做法只在少数几种情况下有用,比如需要循环字符串,可以避免 for 循环的麻烦。
new Array(count + 1).join(stringToRepeat);这句代码的翻译是先创建一个长度为count+1的空数组,然后使用join将数组替换成一个字符串用stringToRepeat提供的连字符。最终的结果就是产生了count长度的连字符串。关于性能的问题,经测试区别不大。
var s = new Date; var test1 = []; for(var i = 0; i < 100000; i++){ test1[i] = []; } var e = new Date; console.log("运行时间:" + (e - s) + "ms"); var s = new Date; var test2 = []; for(var i = 0; i < 100000; i++){ test2[i] = new Array; } var e = new Date; console.log("运行时间:" + (e - s) + "ms"); 2017-03-02 14:43:15.804 VM20486:7 运行时间:33ms 2017-03-02 14:43:15.807 VM20486:14 运行时间:3ms
忽上忽下,甚至后者还较前者少,所以作者的结论不准确。
其他的少数情况是什么?(数组的四种声明方式)
应该还有固定长度但不固定元素。
var a = [];
var b = new Array();
var c = Array();
var d = Object.create(Array.prototype);
传送门:https://segmentfault.com/q/1010000007312888
不过大多数认为选择哪种都行,按需按个人编码习惯而行就好了。
42.类型转换比较==的使用习惯:
上面的表格展示了强制类型转换,这也是使用 == 被广泛认为是不好编程习惯的主要原因, 由于它的复杂转换规则,会导致难以跟踪的问题。此外,强制类型转换也会带来性能消耗,比如一个字符串为了和一个数字进行比较,必须事先被强制转换为数字。这种类型转换个人觉得也的确绝非处处必要。而需要的时候又可以显式转换让代码可读性更强。因此关于==的深入留到以后罢!!
43.这里等于操作符比较的不是值是否相等,而是是否属于同一个身份;也就是说,只有对象的同一个实例才被认为是相等的。 这有点像 Python 中的 is 和 C 中的指针比较。
对象的比较实际比较的是两个的引用类型值(地址值)。只能是指向同一对象才会被认为是相等的。
44.typeof操作符和instanceof操作符。
前者识别原始值没有问题。但是对于引用值只能识别出函数和非函数也即对象。
instanceof是为了判断a是否b的实例的。为了应对继承。
尽管 instanceof 还有一些极少数的应用场景,typeof 只有一个实际的应用(译者注:这个实际应用是用来检测一个对象是否已经定义或者是否已经赋值), 而这个应用却不是用来检查对象的类型。instanceof 操作符用来比较两个操作数的构造函数。只有在比较自定义的对象时才有意义。
typeof a; 如果a未定义或者未赋值都是undefined,而其他都会返回一个非假的值字符串回来。这一特性可以用来判断变量是否存在和赋值。
作者认为比较内置对象使用instanceof没有实际意义。个人认为有失偏颇。
new String('foo') instanceof String; // true new String('foo') instanceof Object; // true 'foo' instanceof String; // false 'foo' instanceof Object; // false
作者以以上代码为例,本来new出来的就是对象,而构造函数String的prototype也是一个对象,因此返回true理所当然。而'foo'是个原始值,虽然可以当对象使用,但实际它不是对象,而是string类型。不是String类。比较为false也是预料之中的。个人觉得基础扎实什么都只是工具,随意取来用就好,没有那么多条条框框。
45.JavaScript 标准文档中定义:[[Class]] 的值只可能是下面字符串中的一个: Arguments, Array, Boolean, Date, Error, Function, JSON, Math, Number, Object, RegExp, String.
其中Number,String,Boolean,null,undefined是原始值。Object,Array,Date,Arguments,Math,RegExp,JSON,Function,Error是引用值。
46.typeof foo !== 'undefined'
上面代码会检测 foo 是否已经定义;如果没有定义而直接使用会导致 ReferenceError 的异常。 这是 typeof 唯一有用的地方。
作者的这个表述不对。foo定义还是未定义,只要是未赋值,都正确返回undefined。
47.Object.prototype.toString被视为检测类型最为可靠的办法。它的检测结果的确仔细。
48.有一点需要注意,instanceof 用来比较属于不同 JavaScript 上下文的对象(比如,浏览器中不同的文档结构)时将会出错, 因为它们的构造函数不会是同一个对象。不同的javascript上下文是因为不同的环境。
这里所指的不同上下文是指iframe等区隔开的不同dom,不同页面之间不同dom。同一文档的script是在同一个全局环境中。
49.''可以转换字符串,+可以转化为数字,!!可以转化为bool值。
50.eval 只在被直接调用并且调用函数就是 eval 本身时,才在当前作用域中执行。
也就是说eval不能被赋值。大概是this指向的问题吧!也就是前面讲到的this丢失。
在任何情况下我们都应该避免使用 eval 函数。99.9% 使用 eval 的场景都有不使用 eval 的解决方案。
51.这个语言也定义了一个全局变量,它的值是 undefined,这个变量也被称为 undefined。 但是这个变量不是一个常量,也不是一个关键字。这意味着它的值可以轻易被覆盖。
52.下面的情况会返回 undefined 值:
- 访问未修改的全局变量
undefined。 - 由于没有定义
return表达式的函数隐式返回。 return表达式没有显式的返回任何内容。- 访问不存在的属性。
- 函数参数没有被显式的传递值。
- 任何被设置为
undefined值的变量。
53.由于全局变量 undefined 只是保存了 undefined 类型实际值的副本, 因此对它赋新值不会改变类型 undefined 的值。为了避免可能对 undefined 值的改变,一个常用的技巧是使用一个传递到匿名包装器的额外参数。 在调用时,这个参数不会获取任何值。
var undefined = 123;
(function(something, foo, undefined) {
// 局部作用域里的 undefined 变量重新获得了 `undefined` 值
})('Hello World', 42);
另外一种达到相同目的方法是在函数内使用变量声明。
var undefined = 123; (function(something, foo) { var undefined; ... })('Hello World', 42);
以上的解释是闭包器传入一个名为undefined的形参变量,它不传值,那么会被解析器赋值一个原始值undefined。保证了undefined的纯正性。不过全局环境下undefined是无法被赋值的,当前浏览器检测是这样。不过也能代表其他浏览器对undefined做了保护。而在函数中
就更未必了。第二种定义一个私有变量,名为undefined,不赋值,解析器同样赋值为原始值undefined。保证undefined的正确使用。
这里唯一的区别是,在压缩后并且函数内没有其它需要使用var声明变量的情况下,这个版本的代码会多出 4 个字节的代码。这丫贵说undefined被分配了4个字节的位置,用于放置undefined值吗。应该是。
54.JavaScript 中的undefined的使用场景类似于其它语言中的 null,实际上 JavaScript 中的null是另外一种数据类型。它在 JavaScript 内部有一些使用场景(比如声明原型链的终结Foo.prototype = null),但是大多数情况下都可以使用undefined来代替。
也就是说null是用于对象的情况,而undefined用途更为广泛。
55.自动的分号插入被认为是 JavaScript 语言最大的设计缺陷之一,因为它能改变代码的行为。缺陷同时是优势,缺陷在于程序员不好的编码习惯,因此保持插入分号的编码习惯才是应该被提倡的。
56.在前置括号的情况下,解析器不会自动插入分号。
log('testing!')
(options.list || []).forEach(function(i) {})
57.建议绝对不要省略分号,同时也提倡将花括号和相应的表达式放在一行(这个值得注意,我常在定义函数时,将{放到下一行), 对于只有一行代码的 if 或者 else 表达式,也不应该省略花括号。 这些良好的编程习惯不仅可以提到代码的一致性,而且可以防止解析器改变代码行为的错误处理。
58.当回调函数的执行被阻塞时,setInterval仍然会发布更多的回调指令。在很小的定时间隔情况下,这会导致回调函数被堆积起来。
也就是说,比如一段代码需要10秒钟,在这中间抛出了一个setTimeout定时器,定时时间为1s,肯定被阻塞,那它会在10s完了之后,就执行。而setInterval在这10s内会每过1s抛一个程序,假设在前面代码5s的时候抛出的,那后面5s将抛出5个代码段在这段10s代码之后,
结果就是10s执行完,后面堆了5个,而且还在不断的抛出,这5个会被忽略,只执行一个。后面的正常执行。
59.处理可能的阻塞调用
最简单也是最容易控制的方案,是在回调函数内部使用 setTimeout 函数。
function foo(){
// 阻塞执行 1 秒
setTimeout(foo, 1000);
}
foo();
这样不仅封装了 setTimeout 回调函数,而且阻止了调用指令的堆积,可以有更多的控制。 foo 函数现在可以控制是否继续执行还是终止执行。
这段话关于封装及控制是没有疑问的。但是阻止调用堆积,说的不够清楚。将setTimeout封装在回调中替换setInterval,放置到上面5s的位置,并执行第一次抛出一个定时器,这个定时器他被阻塞到10s后,等10s执行完,这个定时器被激发,1s后再抛出一个定时器。依此类推。
区别在于一个是执行了定时器,一个只是抛出一个定时器。定时器被阻塞了不要紧,而定时器抛出的程序被阻塞了就会被忽略。
举例:

60.再一种情况:
function foo() { // 将会被调用 } function bar() { function foo() { // 不会被调用 } setTimeout('foo()', 1000); } bar();
由于 eval 在这种情况下不是被直接调用,因此传递到 setTimeout 的字符串会自全局作用域中执行; 因此,上面的回调函数使用的不是定义在 bar 作用域中的局部变量 foo。
建议不要在调用定时器函数时,为了向回调函数传递参数而使用字符串的形式。使用匿名函数。
此时foo只是一个字符串而已,而非变量,因此无法对bar中函数foo进行引用。传入到内部赋值给形参,与外部隔绝。而setTimeout在全局作用域上运行,因而作用域在全局,去全局查找foo就可以理解了。
假设setTimeout的第一个形参名为func,那内部私有的形参变量func被赋值为foo();即func = 'foo()';而func将被在1000ms后使用eval('foo()');执行。此时setTimeout的作用域在全局,因此只能去全局查找foo。

浙公网安备 33010602011771号