前言:准备找实习所以把这个古老的笔记PO上来,虽然标题带着(一)但不知道还有没有更新了,也许写二时候已经是在工作了。由于是本人个人笔记且写于刚开始学JS时候所以写的十分个人化问题也有点那啥,仅作本人不严谨笔记之用途。。
1. [] == [] 这个好理解. 当两个值都是对象 (引用值) 时, 比较的是两个引用值在内存中是否是同一个对象. 因为此 [] 非彼 [], 虽然同为空数组, 确是两个互不相关的空数组, 自然 == 为 false.
[] == ![] 这个要牵涉到 JavaScript 中不同类型 == 比较的规则, 具体是由相关标准定义的. ![] 的值是 false, 此时表达式变为 [] == false, 参照标准, 该比较变成了 [] == ToNumber(false), 即 [] == 0. 这个时候又变成了 ToPrimitive([]) == 0, 【我怀疑这里ToPrimitive([]) 应该==’’】即 '' == 0, 接下来就是比较 ToNumber('') == 0, 也就是 0 == 0, 最终结果为 true.
[] == {} 同第一个.
【注:{}==![]和{}==!{}结果都为false!前者先解析为{}==0,而{}实际上ToString({})为[object Object],所以前后不相等;后者同理】
2. Uncaught TypeError: Cannot read property 'addEventListener' of null
呵呵呵,一点也不strange,但是才学疏浅很容易忽略一点,这里是因为我把脚本放在head里执行了,而此时DOM都还没加载完,,,当然就是null了,用window.onload或者放body最后吧【嗯这确实是当初刚开始学JS时候的笑话】
3. 先有声明后有赋值(提升),即如:console.log(a); var a = 2;实际上在JS引擎编译时,会按照以下顺序处理该代码:
var a;
console.log(a);
a = 2;
所以理所当然的终端会输出undefined,每个作用域都会进行提升操作!要注意的是,函数表达式也属于“赋值”的部分。如:
Foo();
var foo = function bar(){ .. };
会按以下形式处理:
var foo;
Foo(); //typeError,对undefined进行函数调用,所以是typeerror
Bar(); //ReferenceError
Foo = function(){
var bar = ..self..
}
其次,函数会首先被提升,接下来才到变量:
Foo();
var foo;
function foo(){console.log(1);}
Foo = function(){console.log(2);};
被解释为:
function foo..
Foo();
Foo = function....
故此处输出的是1
所以以后真的都得注意在作用域开始就声明变量啦不然麻烦很大
ps.值得注意的是,函数提升仅限于函数声明的情况,对于函数表达式来说不会有函数提升的效果!如:
alert(sum(10,10));
var sum = function(x, y){ //应该换成函数声明function sum(){}才会提升
return x+y;
}
就会提示错误:unexpected identifier
4. 对于如下代码:
var arr1 = ‘john’.split(‘’);
var arr2 = arr1.reverse(); //此处arr1(右边)执行反转操作了哦,且arr2为其
//引用,即,操作arr2就是操作arr1
var arr3 = ‘jones’.split(‘’);
arr2.push(arr3);
结果arr1=arr2数组结果为[‘n’,’h’,’o’,’j’,[‘j’,’o’,’n’,’e’,’s’]]
以后注意下是引用还是副本,另外,右边有操作的话要注意改变与否
由此引用可以引出下面另一个小陷阱:
function foo(){console.log(this.a);}
var a = 2;
var o = {a: 3, foo: foo};
var p = {a: 4};
o.foo(); //3
(p.foo = o.foo)(); //2
看到最后一行没有?输出为2,即this绑定到了全局(默认绑定),因为这里赋值表达式p.foo = o.foo的返回值为目标函数的引用,即调用位置是foo()而非p.foo()或者o.foo()
5. &&运算符:
1&&2返回2而非true或1,引擎先判断左边,嗯,1,但是&&是且,所以还要继续判断,所以判断2,嗯不为0或false,然后返回右边这个值.
PS.如果其中一个操作数为null/NaN/undefined,则返回null/NaN/undefined,但是注意未定义的Undefined和undefined哦,这很容易混淆的,看代码:
var a = true;
var r = (a && b);
console.log(r); //提示b is not defined!
但是呢,不是说操作数为undefiend 就会返回 undefined么!?注意哦,这里的undefined是已经定义了的,但并未赋其初始值的,所以如下代码才能正常运行:
var a = true;
var b; //定义了但未赋值
var r = (a && b);
console.log(r); //返回undefined
6. 对于如下代码:
var a={}, b={key:’b’}, c={key:’c’};
a[b]=123;
a[c]=456;
console.log(a[b]);
输出将会是456而非123!!在设置对象属性时,js会隐式地将参数值转换成字符串,在这种情况下,由于b&c都是对象,所以它们会被转换为“[object Object]”,所以两个是一样的,故会输出最后被覆盖的赋值。由此可以引发下面的一个注意点:
var myObject = {};
myObject[true] = ‘foo’;
myObject[3] = ‘bar’;
myObject[myObject] = ‘baz’;
myObject[‘ture’]; //’true’
myObject[‘3’];//’bar’
myObject[ “[object Object]” ];// ‘baz’
See?在对象中,属性名一定肯定是字符串!如果使用string以外的其他值作为属性名,那它首先会先被转换为字符串,而myObject在之前已经定义为对象了,转换为字符串就是[object Object]
7. 对于如下代码:
var length = 10;
function fn(){
console.log(this.length);
}
var obj = {
length:5,
method: function(fn){
fn();
arguments[0]();
}
};
obj.method(fn,1);
输出会是:
10
2
第一个10不难理解,虽然看起来是obj调用function,但method是obj的函数呀!所以实际上是全局在调用obj的函数method,所以this绑定到了全局上;第二个又有点坑了,且看arguments[0],在运行时候是啥?是fn,所以实际上这里又执行了一次fn,那么this绑定到了哪里呢?可以看到arguments[0]后面跟着()括号(可以这么快速理解虽然不太准确),所以实际上this绑定到了参数数组即arguements上!而参数数组恰好有length,在这里实际上是传递了两个(fn和1)参数,故输出为2
8. 对于.constructor的误导(构造器不是构造器)【还不熟悉】:
function Foo(){/*.....*/}
Foo.prototype = {/*...*/};
var a1 = new Foo();
a1.constructor === Foo; //false
a1.constructor === Object; //true
不要以为在这里是Foo()“构造”了a1所以其.constructor就是指向Foo了!首先,a1并没有.constructor这个属性,所以他会委托[[Prototype]]链上的Foo.prototype,但是在这里这个对象也没有这个属性(默认是有的,但这里第二行代码又创建了一个新的原型对象了,故无),所以他会继续委托,到Object.prototype(见第二行代码),这个对象有该属性,指向Object(..)函数。
所以一般来讲很容易对于js的constructor造成误解,尽管这个词翻译作构造器但并非如词义一般理解,只能说例如a1.constructor等为不可靠不安全的引用,要避免使用。
9. 对于NaN这个特殊的数字类型中的值(它是数字类型哦),尽管字面理解为“not a number”,但实际上翻译为无效数值(反正还是数值)更能准确理解。例如:
var a = 2/’foo’; //NaN
typeof a === ‘number’; //true
且在ES6之前,isNaN()有一个BUG,它连不是number类型的也判定为true!所以如果不用ES6中新的Number.isNaN()函数,则再此之前的polyfill写法为:
if(!Number.isNaN){
Number.isNaN = function(n) {
return (
typeof n === ‘number’ && window.isNaN(n)
);
};
}
或利用其非自反性:
return n !==n;
因为NaN是一个JS中唯一一个非自反的值
10. 得明白这么一个简单基础但十分容易被忽略的“定理”:
简单值(or标量基本类型值),是通过 值复制 来赋值/传递【null, undefined, 字符串,数字,布尔,以及ES6的symbol】
复合值,通过 引用复制 来赋值/传递【对象,包括数组、封装对象、函数啥的】
且一直以来我都以为js中得引用跟C系列语言中的指针一样,实则不然,JS中变量不可能成为指向另一个变量的引用!!!!!!!!
明白了这个,现在来看看这个例子:
var a = [1, 2, 3];
var b = a;
a; //[1, 2,3]
b; //[1, 2, 3]这里并非b指向a,而是这俩都指向[1, 2, 3]
b = [4, 5, 6];
a; //[1, 2, 3]
b; //[4, 5, 6]知道上面注释啥意思了吧?改变了b的指向但不意味着改变a的指向,所以a不变b变
可能有时候会混,确实,所以再来看一个更令人困惑的例子:
function foo(x){
x.push(4);
x; //[1, 2, 3, 4]
x = [4, 5, 6];
x.push(7);
x; //[4, 5, 6, 7]
}
var a = [1, 2, 3];
foo(a);
a; //是[1, 2, 3, 4]而非[4, 5, 6, 7]!
在上例中,.push是对原始的操作,所以会改变,然后函数里边的x改变并不会更改原始的a的指向(传参还记得么,相当于函数内隐式声明x = a,给x的是a的一个复本哟)。
总结:只能更改x和a共同指向的值,不能通过引用x来更改引用a的指向。
像上面一样,如果非要更改a为[4, 5, 6, 7],那当然就用函数咯:.length = 0(清空)then .push(xxx)。
好的,再来看一个尽管以为自己理解了但是还是可能会错的例子:
function foo(x) {
x = x+1;
x; //3
}
var a = 2;
var b = new Number(a); //或者Object(a),效果一样
foo(a);
console.log(b);// 是2而非3
同理,标量基本类型是值传递,所以无法更改,所以尽管函数里x=3,但参数b还是2,那么为啥以前看到的都可以更改呢?那是因为直接在b上操作,那么不直接在b上操作怎么更改呢,好的,既然以前是值传递,那么现在就用引用赋值来干,给他封装进一个函数里,然后xx.b = xxx就可以更改啦~
【事后注明:唉当年太蠢了,基本类型放栈,引用类型放堆。没了】
11. 如下:
var a = new Boolean(false);
if(!a) {
console.log(‘Oooops’); //不会执行
}
why?false的非不是true么?非也,在这里我们给false创建了一个封装对象,而这个对象是真值(always truthy),反正不推荐直接建立封装对象,ya know what,很多方法如.length啥的,直接声明var a = ‘xxx’;时又不是声明对象怎么会有这么个长度的属性呢?所以其实在JS中,会隐式的给你的a封装上对应其类型的一个封装对象,然后调用这个对应类型的对象的方法。知道这个就好
12. JavaScript 与其他语言的(如 Java)的重要区别是在 JavaScript 中语句块(blocks)是没有作用域的,只有函数有作用域。因此如果在一个复合语句中(如 if 控制结构中)使用 var 声明一个变量,那么它的作用域是整个函数(复合语句在函数中)。 但是从 ECMAScript Edition 6 开始将有所不同的, let 和 const 关键字允许你创建块作用域的变量。
所以,要明白直接在全局里for(){var xx}创建的var变量在全局内都是有效的,因为只有函数有作用域,所以以后一般都用函数给封装起来或者在里头用let来声明变量。
13. (不算陷阱,只是记录下来待后来参看)首先要明白,在闭包的外层函数中,这个函数返回一个新创建的函数,通常来说,JS回收器会回收外层函数创建的作用域对象,但是由于返回的函数保留着一个指向那个作用域对象(没错就是对象)的引用,所以这个作用域对象不会被回收,直到外层函数返回的内部函数对象的引用计数为零为止(这也是闭包能保存状态的原因)。
然而这样子也有危险,看到上面那段话的回收条件没?“直到引用计数为零”,所以如果不小心让两者(JS对象和本地对象)形成相互引用时,就发生内存泄漏没办法回收了!
看下面:
function leakMemory() {
var el = document.getElementById('el');
var o = { 'el': el };
el.o = o;
}
//以上为典型错误模板,o和el相互引用
//以下为例子
function addHandler() {
var el = document.getElementById('el');
el.onclick = function() {
el.style.backgroundColor = 'red';
}
}
对 el 的引用不小心被放在一个匿名内部函数中。这就在 JavaScript 对象(这个内部函数)和本地对象之间(el)创建了一个循环引用,所以引发内存泄漏,这种情况很常见但是却容易忽视,所以学会使用this来解决(即,不使用el变量):
function addHandler(){
document.getElementById('el').onclick = function(){
this.style.backgroundColor = 'red';
};
}
好烦啊这个闭包。。。。
15. 对于剩余参数和箭头函数的一个小概念
function foo(){
var f = (...args) => args[0];
return f(2); //the point
}
foo(1);
刚开始时候看到foo形参是空着的所以误以为箭头符号中的剩余参数...args = foo的arguments(当然,的数组化,毕竟arguments是对象),所以以为args[0]=arguments[0]。。
!!!写到这儿的时候发现我理解错了!!因为foo根本没定义参数,所以最后那行的foo(1)只不过是为了调用这个函数进而执行其中的内容(即箭头函数)而已!所以foo的参数是啥都没关系。所以return f(2),参数只有一个所以返回的args[0]当然就是2了。。stupid
PS.对于箭头函数来讲,它没有自身的this,所以当你在某个对象中定义i,然后再定义一个方法,用箭头函数实现,其中返回this.i,理所当然不会返回对象中的i,它的this是继承于上一个作用域的this,so....
PS..如果返回的是的对象字面量,记得用括号扩起来如:()=>({foo:1, b:2});
16. 对于let来说,它跟var一样同样拥有“ 变量提升的效果”,但不同的是,var的创建会设置一个初始的undefined值,而let再没运行到声明的代码时是不会被初始化的,直到初始化执行,let变量都处于一个叫暂存死区(temporal dead zone)的状态,如下:
function sb(){
console.log(bar); //undefined
console.log(foo); //ReferenceError
var bar = 1;
let foo = 2;
}
所以如下两种情况也是处于死区,作为参考:
function sb1(){
var foo = 33;
if(ture){
let foo = (foo + 55); //括号里的foo还是会被解析为if块里的foo
//本来就没创建,现在又赋值,死循环,仍为死区
}
}
function sb2(n){
//注意形参n哦,已经默认定义了
console.log(n); //Object {a: [1,2,3]}
for(let n of n.a){
console.log(n); //其实跟上面是一样的,n.a中的n被解析为for块
//中的n,所以一样是死区
}
}
sb2({a:[1,2,3]});
PS.对于const来说,暂存死区的概念同样适用(跟let一样),因为const和let一样,都是属于块级作用域,如下代码:
const MY_FAV = 7;
MY_FAV = 20; //报错
let MY_FAV = 20; //报错
if(MY_FAV === 7){ //没问题哦,毕竟是块级作用域
let MY_FAV = 20; //虽然有点诡异,但是这没问题的!
console.log(MY_FAV); //输出20
var MY_FAV = 20; //跟let的不同,这个提升引发报错
}
console.log(MY_FAV); //输出7
17. 在函数的参数中如果输入一个对象(即引用类型而非值类型),那我们知道,在函数里改变这个对象的某个属性值,那函数外它也会变(引用嘛),所以这就说明,在函数参数中,引用类型是按引用值传递的?非也!如下例子:
function setName(obj) {
obj.name = “sb”;
obj = new Object();
obj.name = “jianglin”;
}
var person = new Object();
setName(person);
alert(person.name); // 输出sb
可以看到,输出name值并非jianglin。如果person是按引用传递的,那么person就应该被修改为指向name为jianglin的那个新对象,but not。实际上,在函数内部重写obj时,这个变量引用的就是一个新的、函数内的局部对象了,而该局部对象在函数执行完后会被销毁。
18. 一个try catch代码如下:
(function(){
try{
throw new Error();
}catch(x){
var x=1, y=2;
console.log(x); //1
}
console.log(x); //undifined
console.log(y); //2
})();
首先一个注意点是变量声明提升,应该没问题吧,var x, y;提升到作用域顶部,即try的上面。
然后呢,那不应该最后输出y的时候也应该是undifined么,亦或是最后输出的x也是1?请注意,try&catch虽然没有函数作用域,但是在catch中,它的参数却拥有跟函数作用域内变量一样的效果!而里面的其他变量都符合普通语句块的规则(即不在函数作用域内),一般称catch的这个语句块为“伪块作用域”。所以在这个例子中,x所处的作用域是特殊的函数作用域,故在函数作用域内赋的值外边当然获取不了。而y并非catch语句块的参数,所以JS会把它当成普通的语句块内变量来处理,所以y在里面赋值在外面也能获取到。
19. 求求你以后别再if(xx.indexOf(xxx))了!!!-1也能通过的!!用>=0来判断!!【哈哈哈哈哈哈笑死我了】