最近在整理JavaScript的知识,确切的说是在梳理ECMAScript制定的标准,写到笔记里以备回顾。
山高人为峰,有龙则灵。—— http://www.cnblogs.com/litaiqing (此文原地址) 2016-09-06 【大学毕业第1个半月总结JavaScript记】
1.闭包(closure)
这个特性无非是一个热门内容,去搜一下“JavaScript闭包”,会发现一万个人一万个理解,当然理解的深浅也是不一样,错误也不少。
几乎每个JavaScript面试官都会问到这个问题,通常会让你先说出闭包定义,然后写个闭包来说明。
(总结无非是把接受的信号按照自己的设定格式化,所以下面就假设我被面试了。)
a.闭包是什么:
指的是词法表示包括不被计算的变量的函数,也就是说,函数可以使用函数之外定义的变量。(这句话来自w3school)
意味着当前作用域总是能够访问外部作用域中的变量。
谁让现在面试的套路那么多呢,如果那个面试官也是个一知半解,按照上述回答,或许会认为你背下了概念吧。+ _ +~
所以:
四个字形容闭包,吃里扒外!
曾经看到一篇国外的文章问到闭包时有下面这句话:“如果你不能向一个六岁的孩子解释清楚,那么其实你自己根本就没弄懂。”
我想六岁的孩子也会理解吃里扒外吧。
(还有什么比中国成语更好的语言吗?所以在汉语圈天天秀英语,脑袋会退化。这种人算不算个吃里扒外的闭包呢...)
因为 函数 是 JavaScript 中唯一拥有自身作用域的结构,因此闭包的创建依赖于函数。 (所以有下面的 b)
b.写个闭包
一般都会去这样写:
1 function a() { // 不要对我起名字这件事耿耿于怀,毕竟起名字要比吹牛难得多 2 var i = 0; 3 function b(){ 4 return ++i; 5 }; 6 return b(); 7 };
或者写个匿名的:
这种匿名函数有个专词叫 匿名包裹器 ,这种自执行匿名函数会获得变量的一个拷贝,所以如果把匿名包裹器去掉,你会得不到想要的结果。
1 for(var i = 0; i < 10; i++) { // 来自JavaScript神秘花园,实在想不出比这行代码更能体现闭包作用的代码了~ 2 (function(e) { // 匿名包裹器 3 setTimeout(function() { 4 console.log(e); 5 }, 1000); 6 })(i); 7 };
但是上面的代码是谈闭包就会想起来吧,因为面试官的心理准备就是这些东西了。要攻其不备,于是:
1 var name = ‘litaiqing‘; 2 function(){ 3 console.log(name); 4 };
这就是个闭包,是的,你没有看错, 这是 ECMAScript 中使用全局变量是一个简单的闭包。
让我们再看看闭包的定义:函数可以使用函数之外定义的变量。显而易见上述代码是个闭包。
这行简单的代码就诠释了闭包的基本定义,当前作用域总是能够访问外部作用域中的变量,
然后和面试官谈及闭包的一些作用,比如模拟私有变量,避免引用错误等,此时再谈及最上面两个代码,一定会让面试官深信你真懂得闭包。
也就是说,闭包不光能访问自身作用域的变量(吃里),而且还可以使用自身作用域之外的变量(扒外)。
2.var
曾经看过一个培训机构的视频,里面说到JavaScript是弱类型语言,所以声明变量可以不用var
(此刻他正在一个函数中写(私有)变量而且没有var,或许是只是想表达可以不写var吧。)
那么var写于不写有什么区别呢?
var的作用无非是声明变量。但是要注意ECMAScript 的解释程序遇到未声明过的标识符时,用该变量名创建一个全局变量,并将其初始化为指定的值。
所以在函数内部去定义一个私有变量,一定要加上var,这也是体现专业的一点。除非确实需要在外部引用内部修改的全局变量。
例子:(运行一下,不就什么都明白了吗~自己动手,丰衣足食。)
1 var a = 0; 2 (function(){ 3 a = 1; // 不加var,提升为全局变量 4 })(); 5 console.log(a); // 1 6 // ----------------------------------- 7 (function(){ 8 var b = 1; 9 })(); 10 console.log(typeof b); // undefined 11 console.log(b); // b is not defined
函数内不使用 var 关键字声明变量将会覆盖外部的同名变量。 所以不写var是个要格外谨慎的动作。
3.变量声明提升(Hoisting)
在第2点中说到的声明变量用var是不是很有意义呢,但是变量的声明还会有提升的规则。不多说先来段代码:
1 // 来自 Nettuts+ 的一段代码,生动的阐述了 JavaScript 中变量声明提升规则 2 var myvar = ‘my value‘; 3 (function() { 4 alert(myvar); // undefined 5 var myvar = ‘local value‘; 6 })();
为什么myvar会是undefined呢?这是因为JavaScript 会提升变量声明。这意味着 var 表达式和 function 声明都将会被提升到当前作用域的顶部。
上述代码在运行时,会变成如下:
1 // 来自 Nettuts+ 的一段代码,生动的阐述了 JavaScript 中变量声明提升规则 2 var myvar = ‘my value‘; 3 (function() { 4 var myvar; // 移动到当前作用域的顶部,此时myvar显然为undefinded. 5 alert(myvar); // undefined JavaScript会默认先从当前作用域查找变量 6 myvar = ‘local value‘; 7 })();
4.作用域与命名空间
作用域是什么?作用域是指变量的适用范围。
ECMAScript 只有公用作用域
对 ECMAScript 讨论公用和私有作用域几乎毫无意义,因为 ECMAScript 中只存在一种作用域 - 公用作用域。
ECMAScript 中的所有对象的所有属性和方法都是公用的。
因此,定义自己的类和对象时,必须格外小心。记住,所有属性和方法默认都是公用的!同时严格来说,ECMAScript 并没有静态作用域。(w3school)
那么我们如何去写一个作用域呢?
JavaScript 支持一对花括号创建的代码段,但是并不支持块级作用域; 而仅仅支持函数作用域。
function test() { // 一个作用域 (来自JavaScript秘密花园) for(var i = 0; i < 10; i++) { // 不是一个作用域 // count }; console.log(i); // 10 // i在test这个函数作用域里 };
JavaScript 中没有显式的命名空间定义,这就意味着所有对象都定义在一个全局共享的命名空间下面。(上述ECMAScript的特性。)
这样就有了以下的问题(称为问题不太确切,不过也确实引起了问题)
每次引用一个变量,JavaScript 会向上遍历整个作用域直到找到这个变量为止。 如果到达全局作用域但是这个变量仍未找到,则会抛出 ReferenceError 异常。
(记住这点,因为第5点要谈论For In!~)
5.for in 循环,你喜欢用吗?
首先将第4点最后一句的话拿下来:
每次引用一个变量,JavaScript 会向上遍历整个作用域直到找到这个变量为止。 如果到达全局作用域但是这个变量仍未找到,则会抛出 ReferenceError 异常。
这句话就是为什么不用for in的原因。
1 // 修改 Object.prototype (来自JavaScript秘密花园~) 2 Object.prototype.bar = 1; 3 4 var foo = {moo: 2}; 5 for(var i in foo) { 6 console.log(i); // 输出两个属性:bar 和 moo // 不信运行一下咯~ 7 };
上述代码中,我们仅仅是打印foo的属性moo,为什么会出现属性bar呢?
JavaScript 是唯一一个被广泛使用的基于原型继承的语言。显然foo继承了Object的原型上的属性和对象。
当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止。
到查找到达原型链的顶部 - 也就是 Object.prototype - 但是仍然没有找到指定的属性,就会返回 undefined。
>>>>>> 强行插入:
在 JavaScript 核心语言中,全局对象的预定义属性都是不可枚举的,所有可以用 for/in 循环列出所有隐式或显式声明的全局变量。
1 for (var k in this) { // 运行一下咯~ 2 console.log(k); 3 };
<<<<<
所以在这个for-in循环中,in操作会向上遍历属性,于是bar 也输出了。
那么如果仅仅想输出foo自身的属性(确切的说是{moo:2}这个对象)呢?
于是,我们就要去说明一下hasOwnProperty 函数。
hasOwnProperty 是 JavaScript 中唯一一个处理属性但是不查找原型链的函数。
为了判断一个对象是否包含自定义属性而不是原型链上的属性, 我们需要使用继承自 Object.prototype 的 hasOwnProperty 方法。
当检查对象上某个属性是否存在时,hasOwnProperty 是唯一可用的方法。
于是上面的for-in解决办法为:
1 // 修改 Object.prototype (来自JavaScript秘密花园~) 2 Object.prototype.bar = 1; 3 4 var foo = {moo: 2}; 5 for(var i in foo) { 6 if(foo.hasOwnProperty(i)){ 7 console.log(i); // 输出一个属性:moo 8 }; 9 };
由于 for in 总是要遍历整个原型链,因此如果一个对象的继承层次太深的话会影响性能。
另一注意点:
JavaScript 不会保护 hasOwnProperty 被非法占用,因此如果一个对象碰巧存在这个属性, 就需要使用外部的 hasOwnProperty 函数来获取正确的结果。
1 var foo = { // (来自JavaScript秘密花园) 2 hasOwnProperty: function() { 3 return false; 4 }, 5 bar: ‘Here be dragons‘ 6 }; 7 8 foo.hasOwnProperty(‘bar‘); // 总是返回 false 9 10 // 使用其它对象的 hasOwnProperty,并将其上下为设置为foo 11 {}.hasOwnProperty.call(foo, ‘bar‘); // true
如果要遍历的对象是个数组,一定要先缓存这个数组的长度,然后再用普通for-length遍历!否则数组越大,性能越差。
由于 for in 循环会枚举原型链上的所有属性,唯一过滤这些属性的方式是使用 hasOwnProperty 函数, 因此会比普通的 for 循环慢上好多倍。
1 Object.prototype.bar = 1; // 原型继承的双刃剑~~ 2 var list = [1, 2, 3, 4, 5, ...... 100000000]; 3 // 性能优,而且不会出现bar属性 4 for(var i = 0, l = list.length; i < l; i++) { 5 console.log(list[i]); 6 }; 7 // 性能差,会出现bar属性 8 for(var k in list) { 9 console.log(k); 10 };
6. "==" PK "==="
使用 == 被广泛认为是不好编程习惯。
JavaScript 是弱类型语言,这就意味着,等于操作符会为了比较两个值而进行强制类型转换。
所以,在JavaScript里所有的对象比较最后都会强制类型转换为数值,然后进行比较。所以会带来性能消耗!!!
来一个最经典的例子好了。
1 ‘‘ == ‘0‘; // false 2 ‘‘ == 0; // true 3 ‘0‘ == 0; // true
== 的比较过程遵循以下原则:
执行类型转换的规则如下:
1. 如果一个运算数是 Boolean 值,在检查相等性之前,把它转换成数字值。false 转换成 0,true 为 1,(这点和C语言很像吧)。
2. 如果一个运算数是字符串,另一个是数字,在检查相等性之前,要尝试把字符串转换成数字。
3. 如果一个运算数是对象,另一个是字符串,在检查相等性之前,要尝试把对象转换成字符串。
4. 如果一个运算数是对象,另一个是数字,在检查相等性之前,要尝试把对象转换成数字。
5. 如果两个运算数都是字符串,则会逐个比较字符串中每个字符的ASCII码。(这是我自己加的,上面4条来自w3school,加上这条会更好!)
(两个运算数使用==比较,只要有一个是数字或者Boolean,就会将两个运算数都会尝试转换为数字!如果无法转换就会报错,例如:{} == 0)
在比较时,该运算符还遵守下列规则:
6. 值 null 和 undefined 相等。
7. 在检查相等性时,不能把 null 和 undefined 转换成其他值。
8. 如果某个运算数是 NaN,等号将返回 false,非等号将返回 true。
9. 如果两个运算数都是对象,那么比较的是它们的引用值。如果两个运算数指向同一对象,那么等号返回 true,否则两个运算数不等。
重要提示:即使两个数都是 NaN,等号仍然返回 false,因为根据规则,NaN 不等于 NaN。
所以:
1 ‘‘ == ‘0‘; // false 2 /* 3 1. ‘‘ == ‘0‘ 都是字符串,转换为转成ASCII码 4 2. 显而易见,‘‘、‘0‘ ASCII码是不相等的(‘‘ < ‘0‘ // true )。 返回false 5 */ 6 0 == ‘‘; // true 7 /* 8 1. 0 == ‘‘ 符合第2条规则,于是先把字符串转为数字。 9 2. Number(‘‘) 会转化为 0 10 3. 于是 0 == 0 返回true 11 */ 12 ‘0‘ == 0; // true 13 /* 14 1. ‘0‘ == 0 符合第2条规则,于是先把字符串转为数字。 15 */
现在要说一下 ‘==’ PK ‘===’ 的事了。
与==不同的是===不会进行类型转换,所以也不会有多余的性能消耗。
1 ‘‘ === ‘0‘; // false 2 ‘‘ === 0 ; // false 3 ‘0‘ === 0 ; // false
上述结果没什么好讨论的。
严格等于操作符要注意的一点是 当其中有一个操作数为对象时,只有对象的同一个实例才被认为是相等的。
所以 使用===要比==更好一些,如果类型需要转换,应该在比较之前显式的转换, 而不是使用语言本身复杂的强制转换规则。
7. “ ; ” 的力量
有没有注意到,上面写的代码中,所有该结束的地方都加上了“;”。
有人说JavaScript可以不写分号结束,换行就可以,写起来还舒服。
此人说的正确与否不必讨论,那么JavaScript到底需不需要分号呢,答案是 需要 。
这是因为JavaScript解析器它需要分号来就解析源代码。
JavaScript 解析器在遇到由于缺少分号导致的解析错误时,会自动在源代码中插入分号。
注意!在前置括号的情况下,解析器不会自动插入分号。
所以,如果不写分号,就会有以下情况:
1 var a = function(){ 2 console.log(‘run‘) // 没有分号 3 } 4 a() // 没有分号 5 alert(‘testing!‘) // 没有分号 6 ([]).forEach(function(i) {}) // 没有分号
那么对于上述这些代码,JavaScript在解析时需要自己判断需要在哪些地方插入分号。
1 var a = function(){ 2 console.log(‘run‘); // 加上分号 3 }; 4 a(); // 加上分号 5 // 下面两个代码由于([])是前置括号的情况,所以两句代码中间解析器不会自动插入分号!!! 6 alert(‘testing!‘)([]).forEach(function(i) {}); // 加上分号
解析器显著改变了上面代码的行为,在另外一些情况下也会做出错误的处理。
运行此代码发现,在alert()函数运行结束后,会抛出 alert(...) is not a function(…) 这样的错误!
同时:JavaScript 不能正确的处理 return 表达式紧跟换行符的情况, 虽然这不能算是自动分号插入的错误,但这确实是一种不希望的副作用。
1 function add(a, b){ 2 return 3 a + b; // 注意此处与return不在同一行。 4 }; 5 console.log(add(1, 2)); // undefined
自动分号插入被认为是 JavaScript 语言最大的设计缺陷之一,因为它能改变代码的行为。
所以我们要在每个需要分号的地方都要加上分号!(这样最好。)
8.JavaScript高手代码举例(这里说技巧代码比较合理,但是谁让我吹起来了呢~)
除了上面7点的简略总结,还有很多JavaScript重要的高级特性比如 typeof(或许是 JavaScript 中最大的设计缺陷)、this实质、 继承等知识点就不写了,因为我迫不及待要谈代码了。
想过没有菜鸟和高手到底差在哪里?
基础?经验?学历?...
菜鸟和高手唯一的差别是思想,也就是动不动脑筋,任何人做事只要会动脑筋就可以成为高手。
a. 位运算~
场景:
如果字符串‘abc’中含有‘a’,就弹出yes,否则弹出no。
普通代码一般会这样:
1 if(‘abc‘.indexOf(‘a‘) > -1){ 2 alert(‘yes‘); 3 } else { 4 alert(‘no‘); 5 };
不过 > -1 太难敲了,难受,浑身难受!!
那么就干了这碗大力吧!
1 ~‘abc‘.indexOf(‘a‘)?alert(‘yes‘):alert(‘no‘);
介绍位运算 NOT 过程:
位运算 NOT 是三步的处理过程:(实际上就是 对数字求负,然后减 1)
1.把运算数转换成 32 位数字
2.把二进制数转换成它的二进制反码
3.把二进制数转换成浮点数
我们知道 ‘abc‘.indexOf(‘a‘) 如果包含‘a’(显然是包含),就会返回一个大于-1的值(也就是起始位置角标),否则返回-1
那么如果一个大于-1的数经过一次NOT运算会变成什么呢?
(希望我下面的数学表达式没错吧,毕竟从小就数学不太好,数学符号都忘得差不多了~)
令 x = {x | x > -1,且x ∈ N}, 则有 -x-1 > 0 。
所以只要是包含指定的字符串~‘abc‘.indexOf(‘a‘) 就会返回一个大于 0 的正整数。
那么当x = -1时也就是不包含制定字符串时,就会返回 0。
回到JavaScript中来,对于Number类型的ToBoolean会有以下规则:
Number 如果参数为 +0, -0 或 NaN,则结果为 false;否则为 true。
从而,~‘abc‘.indexOf(‘a‘) 在if 中 的逻辑值就是如果包含字符串就为true,否则为false。
不过更专业一点写法是这样:
1 !!~‘abc‘.indexOf(‘a‘)?alert(‘yes‘):alert(‘no‘);
加两个叹号就专业了?是的,这种做法把数值类型转换为了boolean类型,毕竟JavaScript的隐式转换比较怕怕~~(看第6点)。
b. 逻辑运算符 && ||
&& 有如下特性规则:
如果第一个运算数决定了结果,就不再计算第二个运算数。
如果某个运算数不是原始的 Boolean 型值,逻辑 AND 运算并不一定返回 Boolean 值:
如果一个运算数是对象,另一个是 Boolean 值,返回该对象。
如果两个运算数都是对象,返回第二个对象。
如果某个运算数是 null,返回 null。
如果某个运算数是 NaN,返回 NaN。
如果某个运算数是 undefined,发生错误。
于是: 返回最后一个对象,如果没有对象就返回boolean值。
|| 有如下特性规则:
如果第一个运算数值为 true,就不再计算第二个运算数。
如果某个运算数不是 Boolean 值,逻辑 OR 运算并不一定返回 Boolean 值:
如果一个运算数是对象,并且该对象左边的运算数值均为 false,则返回该对象。如果两个运算数都是对象,返回第一个对象。
如果最后一个运算数是 null,并且其他运算数值均为 false,则返回 null。
如果最后一个运算数是 NaN,并且其他运算数值均为 false,则返回 NaN。
如果某个运算数是 undefined,发生错误。
于是:返回第一个对象,如果没有对象就返回boolean值。
从而:
1 if(~‘abc‘.indexOf(‘a‘)){ 2 alert(‘yes‘); 3 } 4 // 简写为 5 !~‘abc‘.indexOf(‘a‘) || alert(‘yes‘); 6 // 或者 7 ~‘abc‘.indexOf(‘a‘) && alert(‘yes‘);
c. if -else & switch简写
现在有“菜鸟”、“一般”、“专业”、“高手”,码农F4组合各显身手:
场景:
现在给不同年龄的人发放奖金:
如果年龄为 22 岁,则发放2200RMB,
如果年龄为 32 岁,则发放3200RMB,
如果年龄为 42 岁,则发放4200RMB,
如果年龄为 52 岁,则发放5200RMB
菜鸟代码会这样写:
1 var age = 22; 2 var money = 0; 3 if(age === 22){ 4 money = 2200; 5 } else if(age === 32){ 6 money = 3200; 7 } else if(age === 42){ 8 money = 4200; 9 } else if(age === 52){ 10 money = 5200; 11 }; 12 console.log(‘age:‘+age+‘ - money:‘+money);
一般代码会这样写:
1 var age = 22; 2 var money = 0; 3 switch(age){ 4 case 22: 5 money = 2200; 6 break; 7 case 32: 8 money = 3200; 9 break; 10 case 42: 11 money = 4200; 12 break; 13 case 52: 14 money = 5200; 15 break; 16 } 17 console.log(‘age:‘+age+‘ - money:‘+money);
专业代码会这样写:
1 var age = 22; 2 var money = (age === 22 && 2200) || 3 (age === 32 && 3200) || 4 (age === 42 && 4200) || 5 (age === 52 && 5200) || 6 0; 7 console.log(‘age:‘+age+‘ - money:‘+money);
高手代码会这样写:
1 var age = 22; 2 var money = {‘22‘:2200,‘32‘:3200,‘42‘:4200,‘52‘:5200}[age]|| 0; 3 console.log(‘age:‘+age+‘ - money:‘+money);
现在变需求了!
新场景:
现在给不同年龄段的人发放奖金:
如果年龄大于 22 岁,则发放2200RMB,
如果年龄大于 32 岁,则发放3200RMB,
如果年龄大于 42 岁,则发放4200RMB,
如果年龄大于 52 岁,则发放5200RMB
重叠区域按最近区域处理。
菜鸟这样改写:
1 var age = 22; 2 var money = 0; 3 if(age >= 22){ 4 money = 2200; 5 } else if(age >= 32){ 6 money = 3200; 7 } else if(age >= 42){ 8 money = 4200; 9 } else if(age >= 52){ 10 money = 5200; 11 }; 12 console.log(‘age:‘+age+‘ - money:‘+money);
一般这样改写:
1 var age = 22; 2 var money = 0; 3 switch(Math.floor(age/10)){ 4 case 2: 5 money = 2200; 6 break; 7 case 3: 8 money = 3200; 9 break; 10 case 4: 11 money = 4200; 12 break; 13 case 5: 14 money = 5200; 15 break; 16 } 17 console.log(‘age:‘+age+‘ - money:‘+money);
专业这样改写:
1 var age = 22; 2 var money = (age >= 22 && 2200) || 3 (age >= 32 && 3200) || 4 (age >= 42 && 4200) || 5 (age >= 52 && 5200) || 6 0; 7 console.log(‘age:‘+age+‘ - money:‘+money);
高手这样改写:
1 var age = 22; 2 var money = {‘2‘:2200,‘3‘:3200,‘4‘:4200,‘5‘:5200}[Math.floor(age/10)] || 0; 3 console.log(‘age:‘+age+‘ - money:‘+money);
如果让你改写,你会改写成什么样呢?
d. 玩转JavaScript语法
1 // 99 转成字符串 2 // 一般: 3 99 + ‘‘; 4 // 高手: 5 99..toString();//是的你没有看错,是两个点。这也是JavaScript解析器的一个缺陷
1 (function(e){ 2 alert(e); 3 })(123456);// ()()玩烂了吧 4 +function(e){ 5 alert(e); 6 }(123456);// 这种是不是有点新鲜感
时间不多了, 就写到这里吧,好多特性没写出来。2016-09-06 18:26。晚上加班...求解脱。
http://www.cnblogs.com/litaiqing