高级函数
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);