奇怪的JS(一)

前言:准备找实习所以把这个古老的笔记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来判断!!【哈哈哈哈哈哈笑死我了】

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值