首页 > 技术文章 > js模式学习

linst 2017-08-25 18:26 原文

在JavaScript有二个特性,

第一个特性是js可直接使用变量,甚至无需声明。如

    function sum(x,y){
        result = x + y;    //result是全局变量。
       console.log(result);
    }

    sum(1,2);
    console.log(result);    //3

如果使用var可以避免这个:

     function sum(x,y){
      var  result = x + y;
       console.log(result);
    }
    sum(1,2);
    console.log(result);    //报错,找不到这个变量

创建隐式全局变量的反模式是带有var声明的链式赋值

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

   foo();
//   console.log(a);    //a is not defined
   console.log(b);  //0,因为 var a = (b = 0 );此时b未经声明,表达式的返回值是0,在赋给var声明的局部变量a。

对链式赋值的所有变量都进行声明。

 function foo() {
       var a , b;
       a = b =0;    //这样均为局部变量。
   }

   foo();

变量释放时的副作用

  • 使用var创建的全局变量(这类变量在函数外部创建)不能删除
  • 不使用var创建的隐含全局变量(尽管它是在函数内部创建)可以删除
    var global_var = 1;
    global_novar = 2;    //反模式
    (function(){
        global_fromfunc = 3;    //反模式
    }());

    delete global_var;    //false
    delete global_novar;    //true
    delete global_fromfunc;    //true

    console.log(typeof global_var);    //number
    console.log(typeof global_novar);    //undefined
    console.log(typeof global_fromfunc);    //undefined

在ES5 strict模式中,为没有声明的变量赋值会抛出错误(类似上述代码中的两种反模式)

访问全局对象

从内嵌函数的作用域访问

   var global = (function(){
       return this;
   }());

单一var模式(Single var Patten)

只使用一个var在函数顶部进行变量声明是一种非常有用的模式。

    function func() {
       var a = 1,
           b = 2,
           sum = a + b,
           myobject = {},
           i,
           j;
       //函数体。。。

    }

提升:凌散变量的问题

JavaScript允许在函数的任意地方声明多个变量,无论在哪里声明,效果都等同于在函数顶部进行声明。这就是所谓的提升。在JavaScript中,只要变量是在同一个范围(同一函数)里,就视为已经声明,哪怕是在变量声明就使用。

    //反模式
    myname = "global";//全局变量
    function func() {
      alert(myname);//未定义
      var myname = "local";
      alert(myname); //局部变量
    }
    func();

前面的代码片断运行结果和以下代码一样。

    //反模式
    myname = "global";//全局变量
    function func() {
      var myname; //等同于 -> var myname = undefined;
      alert(myname);//未定义
      myname = "local";
      alert(myname);  //局部
    }
    func();

for循环

这种模式的问题在于每次循环迭代时都要访问数据的长度。特别是当myarray不是数据,而是HTML容器对时。

    //次优循环
    for(var i = 0; i< myarray.length; i++) {
      //对myarray[i]的操作
      }

以下代码:在这种方式在,对长度的值值提取一次,但应用到整个循环中。

     for(var i = 0, max = myarray.length; i< max; i++) {
      //对myarray[i]的操作
    }

for-in 循环

    var man = {
        hand: 2,
        legs: 2,
        heads: 1
    };
    if(typeof Object.prototype.clone === "undefined") {
        Object.prototype.clone = function () {};
    }

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

避免使用隐式类型转换

    var zero = 0;
    if (zero === false) {
    // 因为zero是0,而不是false,所以代码未执行
    }
    // 反模式
    if (zero == false) {
        //改代码会被执行。
    }

避免使用eval()

该函数会将任意字符串当做一个JavaScript代码来执行。当需要讨论的代码是预先就编写好了(不是在动态运行时决定),是没有理由需要使用eval()。如果是运行时动态生成的,则也有更好的方法代替eval()。

    //反模式
     var property = "name";
    alert(eval("obj." + property));
    // 推荐的方法
    var property = "name";
    alert(obj[property]);

使用parseInt()的数值约定

该函数的第二个函数是一个进制参数,通常可以忽略该参数,但最后不要这么做。

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

在ECMAScript 3版本中,0开始字符串会被当做一个八进制。而在ECMAScript 6 版本发生了改变。

对象字面量语法

  • 将对象包装在大括号中(左大括号 “{” 和右大括号 “}”)
  • 对象中以逗号分隔属性和方法。
  • 用冒号来分隔属性名称和属性的值
  • 当给变量赋值时,请不要忘记右大括号 “}” 后的分号。

来自构造函数的对象

在下面的例子中展示了以两种等价的方法来创建两个相同的对象:
第一种方法-使用了字面量

    var car = {goes:"far"};

//另一种方法-使用内置构造函数
//反模式

    var car = new Object();
    car.goes = "far";

自定义构造函数

下面是对Person构造函数的定义:

    var Person = function (name) {
        this.name = name;
        this.say = function () {
            return "I am" + this.name;
        };
    };

当new操作符调用构造函数的时候,函数内部会发生以下情况:

  • 创建一个空对象并且this变量引用了该对象,同时还继承了该函数的原型。
  • 属性和方法被加入到this引用的对象中。
  • 新创建的对象有this所引用,并且最后隐式地返回this(如果没有显示地返回其他对)
    以上情况看起就像在后台发生了如下事情:

      var Person = function (name) {
          // 使用对象字面量模式创建一个新对象
          // var this = {};
    
          // 向this添加属性和方法
          this.name = name;
          this.say = function () {
              return "I am" + this.name;
          }
    
          // return this;
     }
    

    在以上代码中,为了简单起见,将say()方法添加到this中。其造成的结果是在任何时候调用new Person()时都会在内存中创建一个新的函数。这种方法的效率显然非常低下。因为多个实例之间的say()方法实际上没有改变。更好的选择应该是将方法添加到Person类的原型中。

      Person.prototype.say = function() {
          return "I am" + this.name;
      };
    

    以上语句并不是真相的全部。因为空对象实际上并不空,它已经从Person的原型中继承了许多成员。因此,它更像是下面的语句。
    ``//var this = Object.create(Person.prototype);

自调用构造函数

为了解决前面模式的缺点,并使得原型属性可以在实例对象中使用,可以在构造函数中检查this是否为构造函数的一个实例,如果为否,构造函数可以再次调用自身,并且在这次调用中正确地使用new操作符:

    function Waffle() {
        if (!(this instanceof Waffle)) {
            return new Waffle();
        }
        this.tastes = "yummy";
    }
    Waffle.prototype.wantAnother = true;

    //测试调用
    var first = new Waffle(),
        second = Waffle();
    console.log(first.tastes);    // 输出"yummy"
    console.log(second.tastes);    // 输出"yummy"
    console.log(first.wantAnother); // 输出true
    console.log(second.wantAnother); // 输出true

数组字面量

在下面的例子中,可以相同的元素,并以两种不同的方法创建两个数组,即使用Array()构造函数和使用字面量模式。

    // 具有三个元素的数组
    // 反模式
     var a= new Array("itsy","bitsy","spider");
    // 完全相同的数组
    var a = ["itsy","bitsy","spider"];
    console.log(typeof a );    //输出"object",这是由于数组本身也是对象类型。
    console.log(a.constructor === Array);    // true

数组构造函数的特殊性

避开new Array() 的另一个理由是避免构造函数中可能产生的陷阱。
当想Array()构造函数传递单个数字时,它并不会成为第一个数组元素的值。相反,它却设定了数组的长度。

    //具有一个元素的数组
    var a = [3];
    console.log(a.length);  //1
    console.log(a[0]); // 3

    //具有三个元素的数组
    var a = new Array(3);
    console.log(a.length);  // 3
    console.log(typeof a[0]); //输出undefined

上面例子中,可能并非预期的效果,但是与new Array()传递一个整数相比,如果向构造函数传递一个浮点数,则情况变得更加糟糕。

    //使用数组字面量
    var a = [3.14];
    console.log(a[0]); // 3.14

    var a = new Array(3.14); //输出RangeError:invalid array length
    console.log(typeof a);

为了避免在运行时创建动态数组可能产生的潜在错误,坚持使用数组字面量表示法。

即时函数

即时函数模式是一种可以支持在定义函数后立即执行该函数的语法。

      (function(){
            alert('wathc out');
    }());

下面的替代语法也是很常见的,但JSLint偏好使用第一种语法:

    (function(){
        alert('wathc out');
    })();

这种模式非常有用,因为它为初始化代码提供了一个作用域沙箱(sandbox)。

    (function(){
      var days = ['Sun','Mon','Tus','Wed','Thu','Fri','Sat'];
      today = new Date();
      msg = 'Today is '+ days[today.getDay()] + ',' + today.getDate();
      alert(msg);
    }()); // 输出 "Today is Fri, 13"

如果上面这些代码没有包装到即时函数中,那么days,today和msg等变量将会成为全局变量,并遗留在初始化代码中。

即时函数的参数

也可以将参数传递到即时函数中,如下例子:

    (function (who, when) {
        console.log("I met " + who + "  on " + when);
    }("Joe Black", new Date()));

一般情况下,全局对象是以参数形式传递给即时函数的,以便在不使用Window:指定全局作用域限定的情况下可以在函数内部访问该函数,这样将使得代码在浏览器环境之外时具有更好的互操作性。

     (function (who, when) {
      // 通过 `global`访问全局变量
    }(this);

即时函数的返回值:

正如任何其他函数一样,即时函数可以返回值,并且这些返回值也可以分配给变量:

    var result = (function(){
        return 2 + 2;
    }());

另一种语法:

    var result = (function(){
        return 2 + 2;
    })();

下面这个例子中,即时函数返回的值是一个函数,它将分配给变量getResult,并且将简单地返回res值,该值被预计算并存储在即时函数的闭包中:

    var getResutl = (function() {
        var res = 2 + 2;
        return function() {
            return res;
        }
    } ());

当定义对象属性时可以使用即时函数。如果需要定义一个在对象生成期内永远都不会改变的属性,但是在定义它之前需要执行一些工作以找出正确的值。

    var o = {
        message: (function() {
            var who = "me",
            what = "call";
            return what + " " + who;
        }()),
        getMsg: function(0 {}
            return this.message;
        )
    };

//用法
o.getMsg(); //输出call me
o.message; // 输出call me

初始化时分支

        var utils = {
         addListener : function (el, type, fn) {
             if (typeof window.addEventListener === 'function') {
                 el.addEventListener(type, fn, false);
             } else if (typeof document.attachEvent === 'function') { // IE

             } else { // 更早版本的浏览器
                 el['on' + type] = fn;
             }
         },
             removeListener: function (el, type, fn) {

             }
       };

此段代码的问题在于效率比较低下。每次在调用utils.addListener()或utils.removeListener()时,都将会重复地执行相同的检查。

当使用初始化分支的时候,可以在脚本初始化加载时一次性特侧出浏览器特征。此时,可以在整个页面生命期内重定义函数运行方式。下面是一个可以处理这个任务的例子。

      //接口
       var utils = {
           addListener: null,
           removeListener: null
       };
       //实现
       if(typeof window.addEventListener === 'function') {
           utils.addListener =function (el, type, fn) {
               el.addEventListener(type, fn, false);
           };
           utils.removeListener = function (el, type, fn) {
               el.removeEventListener(type, fn, false);
           };
       } else if(typeof document.attachEvent === 'function') { //IE
          utils.addListener('on' + type, fn );
          utils.removeListener('on' + type, fn );
       } else {
           utils.addListener = function (el, type, fn) {
               el['on' + type] = fn;
           };
           utils.removeListener = function (el, type, fn) {
               e['on' + type] = null;
           };
       }

函数属性

    var myFunc = function() {
        var cachekey = JSON.stringify(Array.prototype.slice.call(arguments)),result;
        if (!myFunc.cache[cachekey]) {
            result = {};
            //开销很大的操作
            myFunc.cache[cachekey] = result;
        }
        return myFunc.cache[cachekey];
    };
    // 缓存存储
    myFunc.cache = {};

请注意在序列化过程中,对象的“标识”将会丢失。如果有两个不同的对象并且恰好都具有相同的属性,这两个对象将会共享同一个缓存条目。

配置对象

配置对象模式是一种提供更整洁的API的方法。

    function addPerson(conf);

    var conf = {
        username: "batman",
        first: "Bruce",
        last: "Wayne"
    };
    addPerson(conf);

配置对象的优点在于:

  • 不需要记住众多的参数以及其顺序。
  • 可以安全忽略可选参数。
  • 更加易于阅读和维护
  • 更加易于添加和删除参数。

而配置对象的不利之处在于:

  • 需要记住参数名称。
  • 属性名称无法被压缩。
    当函数创建DOM元素时,这种模式可能是非常有用的,例如,可以用在设置元素的CSS样式中,以为元素和样式可能具有大量可选特征和属性。

Curry化

函数应用

在一些纯粹的函数式编程语言中,函数并不描述被调用,而是描述为应用。

    //定义函数
    var sayHi = function(who) {
        return "Hello" + (who ? ", " + who : "") + "!";
    };
    //调用函数
    sayHi(); //输出Hello
    sayHi('world'); // 输出hello world

    //应用函数
    sayHi.apply(null, ["hello"]); // 输出Hello world!

部分应用

Curry化

    //curry化的add()函数
    // 接受部分参数列表
     function add(x, y) {
        var oldx = x, oldy = y;
        if(typeof oldy === "undefined") {
            return function (newy) {
                return oldx + newy;
            };
        }
        //完全应用
        return x + y;
    }

    //测试
    console.log(typeof add(5));  // 输出 "function"
    console.log(add(3,4));  // 7
        //创建并存储一个新函数
        var add2000 = add(2000);
    console.log(add2000(10)); // 输出2010

更精简的版本

    //curry化的add()函数
    //接受部分参数列表
    function add(x, y) {
        if(typeof y === "undefined") { //部分
            return function (y) {
                return x + y;
            };
        }
        // 完全应用
        return x + y;
    }

下面是一个通用curry化函数的示例

    function schonfinkelize(fn) {
        var slice = Array.prototype.slice,
            stored_args = slice.call(arguments,1);
        return function() {
            var new_args = slice.call(arguments),
                args = stored_args.concat(new_args);
            return fn.apply(null,args);
        }
    }

      //普通函数
        function add(x, y) {
            return x + y;
        }

        //将一个函数curry化以获得一个新的函数
        var newadd = schonfinkelize(add,5);
        console.log(newadd(4)); // 输出9

        console.log(schonfinkelize(add,6)(7)); // 输出13

转换函数schonfinkelize()并不局限于单个参数或者单步Curry化。
下面是更多示例:

        //普通函数
        function add(a, b, c, d, e) {
            return a + b + c + d + e;
        }

        // 可运行于任意数量的参数
        schonfinkelize(add, 1, 2, 3)(5, 5); // 16

        //两步curry化
        var addOne = schonfinkelize(add, 1);
        addOne(10,10,10,10);
        var addSix = schonfinkelize(addOne, 2, 3);
        addSix(5, 5); //输出16

推荐阅读