小昱个人博客
欢迎来到小昱的世界

勤学如春起之苗,不见其增,日有所长;辍学如磨刀之石,不见其损,日有所亏
应该引起注意的一些常见JS基础问题
  • 首页 > >
  • 作者:小昱
  • 2017年9月6日 13:15 星期三
  • 浏览:124
  • 字号:
  • 评论:0
  • 一、关于JS的数据类型

    JS的数据类型包含基本数据类型: Number、String、Boolean、Null、Undefined五种,复合数据类型(引用型)Object,其中Array和Function隶属于Object的子集。

    注意以下几点:

    1. 关于null和undefined

    console.log(null == undefined); // true
    console.log(null === undefined); // false

     

    2. undefined不是系统保留字

     

    3. 关于typeof的返回值

    console.log(typeof 1); // number
    console.log(typeof NaN); // number
    console.log(typeof ''); // string
    console.log(typeof '\n'); // string
    console.log(typeof true); // boolean
    console.log(typeof /\//); // object
    console.log(typeof null); // object
    console.log(typeof {}); // object
    console.log(typeof []); // object
    console.log(typeof new Object()); // object
    console.log(typeof undefined); // undefined
    console.log(typeof function () {}); // function

     

    4. 关于NaN

    NaN == NaN // false

     

    5. undefined 和 is not defined

    如果调用一个已声明但未赋值的变量,返回undefined

    如果是一个已声明的对象,调用此对象不存在的属性,返回undefined

    如果调用一个未声明的变量,报错,提示 xxx is not defined

    let a
    let b = {}
    console.log(a) // undefined
    console.log(b.a) // undefined
    console.log(c) // Uncaught ReferenceError: c is not defined

     

    二、关于全局变量污染及作用域的问题

    1. 未声明即使用的变量创建的隐式全局变量

    function sum(x, y) {
       // 不推荐写法: 隐式全局变量 
       result = x + y;
       return result;
    }

    这里,result未声明即使用,是全局变量,应该改进为:

    function sum(x, y) {
       var result = x + y;
       return result;
    }

     

    2. 任务链进行部分var声明创建的隐式全局变量

    function foo() {
       var a = b = 0;
    }

    此现象发生的原因在于这个从右到左的赋值,首先,是赋值表达式b = 0,此情况下b是未声明的。这个表达式的返回值是0,然后这个0就分配给了通过var定义的这个局部变量a。换句话说,就好比你输入了:

    var a = (b = 0);

    改进为链分配,不会产生任何意料之外的全局变量,如:

    function foo() {
       var a, b;
       // ... a = b = 0; // 两个均局部变量
    }

     

    3. var不能使用delete进行删除

    隐式全局变量和明确定义的全局变量间有些小的差异,就是通过delete操作符让变量未定义的能力。

    • ○ 通过var创建的全局变量(任何函数之外的程序中创建)是不能被删除的。
    • ○ 无var创建的隐式全局变量(无视是否在函数中创建)是能被删除的。

    这表明,隐式全局变量并不是真正的全局变量,但它们是全局对象的属性。属性是可以通过delete操作符删除的,而变量是不能的。

    没有通过var声明的变量(比如 a = 1)相当于是对window添加属性(如 window.a = 1) 

    // 定义三个全局变量
    var global_var = 1;
    global_novar = 2; // 反面教材
    (function () {
      global_fromfunc = 3; // 反面教材
    }());
    
    // 试图删除
    console.log(delete global_var) // false
    console.log(delete global_novar) // true
    console.log(delete global_fromfunc) // true
    
    // 测试该删除
    console.log(typeof global_var) // "number"
    console.log(typeof global_novar) // "undefined"
    console.log(typeof global_fromfunc) // "undefined"

    在ES5严格模式下,未声明的变量(如在前面的代码片段中的两个反面教材)工作时会抛出一个错误。

     

    4. var无块级作用域

    if (true) {
      var a = 1
      let b = 2
    }
    console.log(a) // 1
    console.log(b) // ERROR: b is not defined

     

    5. var声明引起的变量提升

    JavaScript中,你可以在函数的任何位置声明多个var语句,并且它们就好像是在函数顶部声明一样发挥作用,这种行为称为 hoisting(悬置/置顶解析/预解析)。当你使用了一个变量,然后不久在函数中又重新声明的话,就可能产生逻辑错误。对于JavaScript,只 要你的变量是在同一个作用域中(同一函数),它都被当做是声明的,即使是它在var声明前使用的时候。

    console.log(a) // undefined
    var a = 0

    注意,只是声明提升,值并未提升,相当于:

    var a
    console.log(a) // undefined
    a = 0

    一个复杂的例子:

    myname = "global"; // 全局变量
    function func() {
        alert(myname); // "undefined"
        var myname = "local";
        alert(myname); // "local"
    }
    func();

    在这个例子中,很多人可能会以为第一个alert弹出的是”global”,第二个弹出”loacl”。这种期许是可以理解的,因为在第一个alert 的时候,myname未声明,此时函数肯定很自然而然地看全局变量myname,但是,实际上并不是这么工作的。第一个alert会弹 出”undefined”是因为myname被当做了函数的局部变量(尽管是之后声明的),所有的变量声明当被悬置到函数的顶部了。因此,为了避免这种混 乱,最好是预先声明你想使用的全部变量。

    相当于:

    myname = "global"; // global variable
    function func() {
       var myname; // 等同于 -> var myname = undefined;
       alert(myname); // "undefined"
       myname = "local";
       alert(myname); // "local"}
    func();

    代码处理分两个阶段,第一阶段是变量,函数声明,以及正常格式的参数创建,这是一个解析和进入上下文 的阶段。第二个阶段是代码执行,函数表达式和不合格的标识符(为声明的变量)被创建。但是,出于实用的目的,我们就采用了”hoisting”这个概念, 这种ECMAScript标准中并未定义,通常用来描述行为。

     

    结合第4点和第5点,看下这个例子:

    if (false) {
      var a = 0
    }
    console.log(a) // undefined

    由于JS五块级作用域,导致声明提前,但if条件为false,不能对a进行赋值,因此打印出undefined

     

     

    三、关于原型链的问题

    1. 对象循环时出现的问题

    先看看例子

    var man = {
      hands: 2,
      legs: 2,
      heads: 1
    };
    
    // 一个方法添加给了所有对象
    if (typeof Object.prototype.clone === "undefined") {
      Object.prototype.clone = function () {};
    }
    
    for (var i in man) {
      console.log(i, ":", man[i]);
    }

    可以看到打印出来的结果为:

    hands : 2
    legs : 2
    heads : 1
    clone: function()

    在这个例子中,我们有一个使用对象字面量定义的名叫man的对象。在man定义完成后的某个地方,在对象原型上增加了一个很有用的名叫 clone()的方法。此原型链是实时的,这就意味着所有的对象自动可以访问新的方法。

    为了避免枚举man的时候出现clone()方法,你需要应用hasOwnProperty()方法过滤原型属性。如果不做过滤,会导致clone()函数显示出来,在大多数情况下这是不希望出现的。

    修改后的循环代码:

    for (var i in man) {
      if (man.hasOwnProperty(i)) { // 过滤
        console.log(i, ":", man[i]);
      }
    }

    或者使用call调用

    for (var i in man) {
      if (Object.prototype.hasOwnProperty.call(man, i)) { // 过滤
        console.log(i, ":", man[i]);
      }
    }

    严格来说,不使用hasOwnProperty()并不是一个错误。根据任务以及你对代码的自信程度,你可以跳过它以提高些许的循环速度。但是当你对当前对象内容(和其原型链)不确定的时候,添加hasOwnProperty()更加保险些。

    也可以像这样:

    // 警告: 通不过JSLint检测
    var i, hasOwn = Object.prototype.hasOwnProperty;
    for (i in man) if (hasOwn.call(man, i)) { // 过滤
    	console.log(i, ":", man[i]);
    }

     

    四、避免使用eval

    切记: eval是魔鬼

    1. 对象的动态属性

    尽量使用 对象的方括号语法调用动态属性

    let obj = {
      name: 'quanzaiyu'
    }
    
    // 反面示例
    var property = "name";
    console.log(eval("obj." + property));
    
    // 更好的
    var property = "name";
    console.log(obj[property]);

     

    2. 定时器

    同样重要的是要记住,给setInterval(), setTimeout()传递字符串,与使用eval()是类似的,因此要避免。

    // 反面示例
    setTimeout("myFunc()", 1000);
    setTimeout("myFunc(1, 2, 3)", 1000);
    
    // 更好的
    setTimeout(myFunc, 1000);
    setTimeout(function () {
       myFunc(1, 2, 3);
    }, 1000);

     

    3. Function()

    使用Function()构造函数传递字符串,作用与使用eval()是类似。

    使用新的Function()构造就类似于eval(),应小心接近。这可能是一个强大的构造,但往往被误用。如果你绝对必须使用eval(),你 可以考虑使用new Function()代替。有一个小的潜在好处,因为在新Function()中作代码评估是在局部函数作用域中运行,所以代码中任何被评估的通过var 定义的变量都不会自动变成全局变量。另一种方法来阻止自动全局变量是封装eval()调用到一个即时函数中。

    console.log(typeof un);    // "undefined"
    console.log(typeof deux); // "undefined"
    console.log(typeof trois); // "undefined"
    
    var jsstring = "var un = 1; console.log(un);";
    eval(jsstring); // logs "1"
    
    jsstring = "var deux = 2; console.log(deux);";
    new Function(jsstring)(); // logs "2"
    
    jsstring = "var trois = 3; console.log(trois);";
    (function () {
      eval(jsstring);
    }()); // logs "3"
    
    console.log(typeof un); // number
    console.log(typeof deux); // "undefined"
    console.log(typeof trois); // "undefined"

    另一间eval()和Function构造不同的是eval()可以干扰作用域链,而Function()更安分守己些。不管你在哪里执行 Function(),它只看到全局作用域。所以其能很好的避免本地变量污染。在下面这个例子中,eval()可以访问和修改它外部作用域中的变量,这是 Function做不来的(注意到使用Function和new Function是相同的)。

    (function () {
      var local = 1;
      eval("local = 3; console.log(local)"); // 3
      console.log(local); // 3
    }());
    
    (function () {
      var local = 1;
      Function("console.log(typeof local);")(); // undefined
      Function("local = 3; console.log(local)")(); // 3
      console.log(local); // 1
    }());

     

    4. 解析json

    let obj = '{a: 0,b: 1}'
    console.log(eval('(' + obj + ')'))

    这里,使用括号的原因是将之当成分组操作符,也就是这对括号,会让解析器强制将JSON的花括号解析成表达式而不是代码块。

    作为替代,推荐使用JSON.parse

     

    五、类型转换中可能出现的问题

    1. parseInt

    使用parseInt()你可以从字符串中获取数值,该方法接受另一个基数参数,这经常省略,但不应该。当字符串以”0″开头的时候就有可能会出问 题,例如,部分时间进入表单域,在ECMAScript 3中,开头为”0″的字符串被当做8进制处理了,但这已在ECMAScript 5中改变了。为了避免矛盾和意外的结果,总是指定基数参数。

    var month = "06",
        year = "09";
    month = parseInt(month, 10);
    year = parseInt(year, 10);

    如果你忽略了基数参数,如parseInt(year),返回的值将是0,因为“09”被当做8进制(好比执行 parseInt( year, 8 )),而09在8进制中不是个有效数字。

    为了避免忘记带基数参数,可以使用以下方法代替:

    console.log(+'08')
    console.log(Number("08"))

    而且这些方法通常快于parseInt()。

     

    六、函数

    1. 函数声明和函数表达式

    函数声明:

    function 函数名称 (参数:可选){ 函数体 }

    函数表达式:

    function 函数名称(可选)(参数:可选){ 函数体 }

    可以看出,如果不声明函数名称,它肯定是表达式,可如果声明了函数名称的话,如何判断是函数声明还是函数表达式呢?ECMAScript是通过上下文来区分的,如果function foo(){}是作为赋值表达式的一部分的话,那它就是一个函数表达式,如果function foo(){}被包含在一个函数体内,或者位于程序的最顶部的话,那它就是一个函数声明。

    function foo(){} // 声明,因为它是程序的一部分
    var bar = function foo(){}; // 表达式,因为它是赋值表达式的一部分
    
    new function bar(){}; // 表达式,因为它是new表达式
    
    (function(){
      function bar(){} // 声明,因为它是函数体的一部分
    })();
    function foo(){} // 函数声明
    (function foo(){}); // 函数表达式:包含在分组操作符内
    
    try {
    	(var x = 5); // 分组操作符,只能包含表达式而不能包含语句:这里的var就是语句
    } catch(err) {
    	// SyntaxError
    }

     

    2. 命名函数表达式

    var f = function foo(){
      return typeof foo; // foo是在内部作用域内有效
    };
    // foo在外部用于是不可见的
    typeof foo; // "undefined"
    f(); // "function"

    注意到,名字只在新定义的函数作用域内有效,因为规范规定了标示符不能在外围的作用域内有效。

     

    七、闭包

    1. 闭包在列表点击事件中的应用

    先看一下没有任何处理的列表项点击事件

    var elems = document.getElementsByTagName('a');
    for (var i = 0; i < elems.length; i++) {
      elems[i].addEventListener('click', function (e) {
        e.preventDefault();
        alert('I am link #' + i);
      }, 'false');
    }

    这种点击各项的话,弹出的永远是列表项的length-1,由于变量i没背锁定住。相反,当循环执行以后,我们在点击的时候i才获得数值。因为这个时候i才真正获得值。所以说无论点击那个连接,最终显示的i都是列表项的length-1

    针对这种情况,有以下几种处理方法

    ① 将i存到每一个列表项的私有属性列表中

    var elems = document.getElementsByTagName('a');
    for (var i = 0; i < elems.length; i++) {
      elems[i].index = i;
      elems[i].addEventListener('click', function (e) {
        e.preventDefault();
        alert('I am link #' + this.index);
      }, 'false');
    }

    ② 使用闭包(立即调用的函数表达式)

    形式一: 使用立即执行函数包裹整个点击事件

    var elems = document.getElementsByTagName('a');
    for (var i = 0; i < elems.length; i++) {
      (function (lockedInIndex) {
        elems[i].addEventListener('click', function (e) {
          e.preventDefault();
          alert('I am link #' + lockedInIndex);
        }, 'false');
      })(i);
    }

    形式二: 使用立即执行函数包裹点击事件的回调函数

    var elems = document.getElementsByTagName('a');
    for (var i = 0; i < elems.length; i++) {
      elems[i].addEventListener('click', (function (lockedInIndex) {
        return function (e) {
          e.preventDefault();
          alert('I am link #' + lockedInIndex);
        };
      })(i), 'false');
    }

     

     


    参考资料

    深入理解JavaScript系列

    深入理解JavaScript系列 - 博客园 -汤姆大叔

     

      您阅读这篇文章共花了:  
     本文无需标签!
    二维码加载中...
    本文作者:小昱      文章标题: 应该引起注意的一些常见JS基础问题
    本文地址:http://www.xiaoyulive.top/?post=133
    版权声明:若无注明,本文皆为“小昱个人博客”原创,转载请保留文章出处。
    返回顶部| 首页| 碰碰手气| 捐赠支持| 手机版本|后花园

    Copyright © 2016-2017 小昱个人博客 滇ICP备16006294号