js高级技巧总结之高级函数和防篡改对象

高级函数

1.安全的类型检测

        谈到类型检测,可能大家首先想到的就是typeof 或者 instanceof (检测数组Array.isArray(arr))等这些方式,但是这些方法都有自己的局限性,比如说Safari(直至第四版)对正则使用typeof会返回function,instanceof必须要在同一个作用域下,还有现在浏览器开始支持原生JSON对象了(Douglas Crockford定义了一个全局JSON对象),于是检测对象是不是原生就又变得困难了 。

         大家可能想到了一个对象的构造函数名和作用域是无关的,于是就可以使用以下方式判断(比如说数组):

function isArray(obj){
    return Object.prototype.toString.call(obj) == "[object Array]";
}

        像这样,可以判别构造函数名是否为 [object Array],[object Function],[object RegExp],[object JSON]来判定各种类型

2.作用域安全的构造函数

         构造函数就是一个使用New操作符调用的函数,只有使用New调用时,构造函数里面的this对象才会指向实例,如果像如下调用:

function Student(name,age){
    this.name = name;
    this.age = age;
}
var Tom = Student('Tom',12);

访问Tom.name是未定义的,这是因为此时的this指向了window,访问window.name就会得到'Tom'。其实很多时候我们会忘掉使用New操作符去实例化一个对象,造成this晚绑定。如果想避免这种问题就需要使用如下构造方式:

function Student(name,age){
    if( this instanceof Student){
        this.name = name;
        this.age = age;
    } else {
        return new Student(name,age);
    }
}

当然,这样虽然避免了this晚绑定的问题,但是锁定了可以使用该构造函数的环境,当你想要使用构造i函数窃取模式的继承且不使用原型链,那么这个继承就会无效,例如:

function Student(name,age){
    if( this instanceof Student){
        this.name = name;
        this.age = age;
    } else {
        return new Student(name,age);
    }
}

function Xueba(name,age,score){
    Student.call(this,name,age);
    this.score = score;
}

var Tom = new Xueba('Tom',12,99);
console.log(Tom.name);

发现name和age并没有被继承,因为Xueba并非Student的实例。要解决这个问题只需要让Xueba成为Student的实例即可

Xueba.prototype = new Student();

3.惰性载入函数

        因为各个浏览器之间的差异,多数的js代码中包含了很多if语句,重复的执行大量的if语句是很耗费资源的,于是就有了解决方法:惰性载入。何为惰性载入呢,直白说就是在第一次运行的时候得到结果,以后再运行就直接调用第一次获取的结果使用。惰性加载有两种加载方式:在第一次调用的时执行,在页面首次加载时执行,至于那种方法更合适,就要看你的具体需求而定了。举个简单的栗子:

 function getInnerText(element){
            if(typeof element.textContent == "String"){
                return element.textContent;
            } else {
                return element.innerText;
            }
        }

(请忽略这里的判断很少,就当他的if语句很多且每个if里面的逻辑很多)每次调用都会进行一次判断,再看看下面的代码

function getInnerText(element){
            if(typeof element.textContent == "String"){
                getInnerText = function(element){
                    return element.textContent;
                }
            } else {
                getInnerText = function(element){
                    return element.innerText;
                } 
            }
            return getInnerText(item);
        }

第一次调用以上两者都会进行判断,但是不同的是,第二种在第一次掉用后就不需要再次判断了,直接返回结果。假设处于大量判断的代码中,后者是不是就节省了资源(废话一句,可能会有人说,这点资源对于人的感知来说压根感觉不到,但是你想想如果有很多这样类似的判定呢),另外一种写法就是在声明函数的时候就指定适当的函数

var element = document.getElementById("there");
var getInnerText = (function(){
    if(typeof element.textContent == "String"){
          return function(element){
                return element.textContent;
          };
     } else {
          return function(element){
                 return element.innerText;
           } 
     }
      return getInnerText(item);
})();
console.log(getInnerText(element));

4.函数绑定

              简单来说就是在特定的环境中以指定参数调用另一个函数,先看个例子

var handler = {
    msg: "233",
    handleClick: function(event){
        console.log(this.msg);
    }
}

var btn = document.getElementById("my-btn");
btn.addEventListener('click',handler.handleClick,false);

为什么点击后显示的是undefine而不是233 ?此时的handler里的this指向了window,这里我们可以用闭包来解决这个问题

var handler = {
   msg: "233",
   handleClick: function(event){
      console.log(this.msg);
   }
}

var btn = document.getElementById("my-btn");
btn.addEventListener('click',function(event){handler.handleClick(event);},false);

虽然问题解决了,但是有时候闭包会增大代码的理解以及调试难度。ECMAScript5为所有函数定义了一个原生的bind()方法,它接受一个函数和一个环境,并返回一个在给定环境中调用给定函数的函数,并将所有参数传递过去,可能有一些绕口,我们来看一个例子

function bind(fn,context){
    return function(){
        return fn.apply(context,arguments);
    }
}

bind函数的作用就是在闭包中使用apply调用传入的函数,并传递context对象和参数,当调用返回函数时,就会在给定环境中执行被传入的函数并给出所有参数。使用方式如下

function bind(fn,context){
    return function(){
         return fn.apply(context,arguments);
     }
}
var handler = {
     msg: "233",
      handleClick: function(event){
             console.log(this.msg);
      }
}

var btn = document.getElementById("my-btn");
btn.addEventListener('click',bind(handler.handleClick,handler),false);

上面的bind函数只是为了大家方便理解才写出来的,在ECMAScript5中无需我们定义就可直接使用,如下

var handler = {
   msg: "233",
   handleClick: function(event){
        console.log(this.msg);
   }
}

var btn = document.getElementById("my-btn");
btn.addEventListener('click',handler.handleClick.bind(handler),false);

此时被绑定的函数要比普通函数有更多的开销,他们主要用于事件处理程序以及SetTimeout,setInterval等,请在必要时使用。

5.函数柯里化

          函数的柯里化和函数的绑定精密相关,使用方式也是一样的,两者的区别在于前者的函数被调用时,还需要传入一些参数。bind()方法也实现了函数柯里化,例如

var handler = {
    msg: "233",
    handleClick: function(name,event){
       console.log(this.msg+":"+name+":"+event.type);
    }
}

var btn = document.getElementById("my-btn");
btn.addEventListener("click",handler.handleClick.bind(handler,"hello"),false);

防篡改对象

          js共享的本质一直让我们有些头疼,多人开发的项目,你一不小心就修改了别人的代码,甚至是用非兼容重写原生对象。当然你可以通过属性的[[Configurable]],[[Writeable]]等特性改变属性的行为,但是这里还有更简单的方法。

1.不可扩展对象(Object.preventExtensions(obj))

         一般的对象都是可以扩展的,也就是说任何对象都允许添加属性和方法,但是使用Object.preventExtensions()方法可以改变这个行为

var person = {
    name: "Bob",
    age: 12
};
person.score= 99;
console.log(person);
Object.preventExtensions(person);
person.job = "student";
console.log(person);

你会发现调用了Object.preventExtensions()方法后,就不能添加属性了,但是对于已经存在的属性,我们依旧可以修改或者删除

var person = {
    name: "Bob",
    age: 12
};
person.score= 99;
console.log(person);

Object.preventExtensions(person);
person.job = "student";
console.log(person);

person.age= 23;
console.log(person);

delete person.age;
console.log(person);

使用Object.isExtensible(person)可以判定person这个对象是否可以扩展(可扩展返回true,反之false)

2.密封对象(Object.seal(obj))

          ECMAScript5定义的第二个级别就是密封对象,密封对象不可扩展,已有成员的[[Configurable]]已经被设为false,这意味着不能删除属性和方法,但是依然能修改已有属性,使用方式和第一个一样。使用Object.isSealed()方法判断对象是否密封了,被密封了的会返回true.注意,因为密封对象是不可扩展的,所以密封了的对象调用Object.isExtensible()会返回false

3.冻结对象(Object.freeze(obj))

         最严格的就是冻结对象,冻结对象既不可扩展,又是密封,且[[Writeable]]特性定位false,冻结了的对象只可读,使用方式和第一个一样。使用Object.isFrozen()可以检测冻结对象,冻结了的对象返回true, 注意冻结对象既是不可扩展也是密封的,可以调用Object.isSealed()和Object.isExtensible()方法

注意:冻结一个对象,只是一种浅冻结,对于对象里的特殊元素,比如数组,对象等依然可以操作。例如

const teacher = Object.freeze({
    name: "Bob",
    age: 21,
    grade: [2,2,2,3,4]
})
teacher.name = "www";
teacher.grade.push(6);//对象的浅冻结
console.log(teacher);

grade依然可以添加元素。如果你想所有冻结(深冻结),可以参考以下方式

var constantize = function(obj){
    Object.freeze(obj);
    Object.keys(obj).forEach((key,i)=>{
        if(typeof obj[key] === 'object'){
            constantize(obj[key]);
        }
    });
}
const teacher2 = {
    name: "Bob",
    age: 21,
    grade: [2,2,2,3,4]
}
constantize(teacher2);
teacher2.name = "www";
teacher2.grade.push(6);//有些会报错,有些回忽略
console.log(teacher2);

 

你可能感兴趣的:(web前端)