JS高级技巧学习小结
安全类型检测
var isArray = value instanceof Array;
1
以上代码要返回true,value必须是一个数组,而且还必须与Array构造函数在同一个全局作用域中(Array是window的属性)。如果value是在另一个框架中定义的数组,那么以上代码就会返回false.
Demo:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="utf-8"> 5 <title>作用域安全的构造函数</title> 6 </head> 7 <body> 8 <script type="text/javascript"> 9 function Person(name,age,job){ 10 if(this instanceof Person)//这里检测以确保this是Person的实例 11 { 12 this.name=name; 13 this.age=age; 14 this.job=job; 15 }else{ 16 return new Person(name,age,job); 17 } 18 19 } 20 var person1=Person("liujie",23,"master"); 21 console.log(window.name);//"" 22 console.log(person1.name);//liujie 23 var person2=new Person("lisi",21,"student"); 24 console.log(person2.name);//lisi 25 </script> 26 </body> 27 </html> 28 29 Demo2 30 31 <!DOCTYPE html> 32 <html> 33 <head> 34 <meta charset="utf-8"> 35 <title>作用域安全的构造函数</title> 36 </head> 37 <body> 38 <script type="text/javascript"> 39 function Person(name, age, job){ 40 this.name = name; 41 this.age = age; 42 this.job = job; 43 } 44 var person1 = new Person("Nicholas", 29, "Software Engineer"); 45 console.log(person1);//[object Object] 46 console.log(person1.name); //"Nicholas" 47 console.log(person1.age); //29 48 console.log(person1.job); //"Software Engineer" 49 50 var person2 = Person("Nicholas", 29, "Software Engineer"); 51 //这里忽略了new操作符,把构造函数作为普通函数调用 52 console.log(person2); //undefined 因为Person函数没有返回值 53 console.log(window.name); //"Nicholas" 这里this-->window 54 console.log(window.age); //29 55 console.log(window.job); //"Software Engineer" 56 </script> 57 </body> 58 </html>
我们知道,在任何值上调用Object原生的toString()方法,都会返回一个[object NativeConstructorName]格式的字符串。每个类在内部都有一个[[Class]]属性,这个属性中就指定了上述字符串中的构造函数名。
举个例子:
console.log(Object.prototype.toString.call(123));//[object Number]
1
由于原生数组的构造函数名与全局作用域无关,因此使用toString()就能保证返回一致的值。因此,我们可以通过以下函数来进行判断。
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="utf-8"> 5 <title>作用域安全的构造函数</title> 6 </head> 7 <body> 8 <script type="text/javascript"> 9 function Person(name,age,job){ 10 if(this instanceof Person)//这里检测以确保this是Person的实例 11 { 12 this.name=name; 13 this.age=age; 14 this.job=job; 15 }else{ 16 return new Person(name,age,job); 17 } 18 19 } 20 var person1=Person("liujie",23,"master"); 21 console.log(window.name);//"" 22 console.log(person1.name);//liujie 23 var person2=new Person("lisi",21,"student"); 24 console.log(person2.name);//lisi 25 </script> 26 </body> 27 </html> 28 29 Demo2 30 31 <!DOCTYPE html> 32 <html> 33 <head> 34 <meta charset="utf-8"> 35 <title>作用域安全的构造函数</title> 36 </head> 37 <body> 38 <script type="text/javascript"> 39 function Person(name, age, job){ 40 this.name = name; 41 this.age = age; 42 this.job = job; 43 } 44 var person1 = new Person("Nicholas", 29, "Software Engineer"); 45 console.log(person1);//[object Object] 46 console.log(person1.name); //"Nicholas" 47 console.log(person1.age); //29 48 console.log(person1.job); //"Software Engineer" 49 50 var person2 = Person("Nicholas", 29, "Software Engineer"); 51 //这里忽略了new操作符,把构造函数作为普通函数调用 52 console.log(person2); //undefined 因为Person函数没有返回值 53 console.log(window.name); //"Nicholas" 这里this-->window 54 console.log(window.age); //29 55 console.log(window.job); //"Software Engineer" 56 </script> 57 </body> 58 </html>
作用域安全的构造函数
构造函数其实就是一个使用new操作符调用的函数,当使用new调用时,构造函数内部用到的this对象会指向新创建的对象实例。
Person构造函数添加了一个检查并确保this对象是Person实例的if语句,它要么使用new操作符,要么在现有的Person实例环境中调用构造函数。任何一种情况,对象初始化都可以正常进行。
如果this对象不是Person的实例,那么会再次使用new操作符调用构造函数并返回结果。这样就可以确保无论是否使用new操作符,都会返回一个Person的新实例。
Demo1:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="utf-8"> 5 <title>作用域安全的构造函数</title> 6 </head> 7 <body> 8 <script type="text/javascript"> 9 function Person(name,age,job){ 10 if(this instanceof Person)//这里检测以确保this是Person的实例 11 { 12 this.name=name; 13 this.age=age; 14 this.job=job; 15 }else{ 16 return new Person(name,age,job); 17 } 18 19 } 20 var person1=Person("liujie",23,"master"); 21 console.log(window.name);//"" 22 console.log(person1.name);//liujie 23 var person2=new Person("lisi",21,"student"); 24 console.log(person2.name);//lisi 25 </script> 26 </body> 27 </html> 28 29 Demo2 30 31 <!DOCTYPE html> 32 <html> 33 <head> 34 <meta charset="utf-8"> 35 <title>作用域安全的构造函数</title> 36 </head> 37 <body> 38 <script type="text/javascript"> 39 function Person(name, age, job){ 40 this.name = name; 41 this.age = age; 42 this.job = job; 43 } 44 var person1 = new Person("Nicholas", 29, "Software Engineer"); 45 console.log(person1);//[object Object] 46 console.log(person1.name); //"Nicholas" 47 console.log(person1.age); //29 48 console.log(person1.job); //"Software Engineer" 49 50 var person2 = Person("Nicholas", 29, "Software Engineer"); 51 //这里忽略了new操作符,把构造函数作为普通函数调用 52 console.log(person2); //undefined 因为Person函数没有返回值 53 console.log(window.name); //"Nicholas" 这里this-->window 54 console.log(window.age); //29 55 console.log(window.job); //"Software Engineer" 56 </script> 57 </body> 58 </html>
特别注意:这里问题在于没有使用new操作符来调用该构造函数的情况上,由于该this对象是在运行时绑定的,所以直接调用Person(),this会映射到全局对象window上,导致错误对象属性的意外增加。
这里原本针对Person实例的三个属性被加到window对象上,因为构造函数是作为普通函数调用的,忽略了new操作符。这个问题是由于this对象的晚绑定造成的,在这里this被解析成了window对象。由于window的name属性是用于识别链接目标和frame的,所以这里对该属性的偶然覆盖可能会导致该页面上出现其他错误。可以创建一个作用域安全的构造函数来解决这个问题。
Demo3
在实现了作用域安全的构造函数后,如果使用构造函数窃取模式的继承(在子类中调用父类的构造函数,通过这种方式给子类添加属性和方法)且不使用原型链,那么这个继承可能被破坏。
下面的代码,Polygon构造函数是作用域安全的,然而Rectangle构造函数则不是。新创建一个Rectangle实例后,这个实例应该通过Polygon.call()来继承Polygon的sides属性。但是,由于Polygon构造函数是作用域安全的,this对象并非Polygon的实例,所以会创建并返回一个新的Polygon对象。Rectangle构造函数中的this对象并没有得到增长,同时Polygon.call()返回的值也没有用到,所以Rectangle实例中就不会有sides属性。
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <title>作用域安全的构造函数</title> 6 </head> 7 <body> 8 <script type="text/javascript"> 9 function Polygon(sides){ 10 if (this instanceof Polygon) { 11 this.sides = sides; 12 this.getArea = function(){ 13 return 0; 14 }; 15 } else { 16 return new Polygon(sides); 17 } 18 } 19 20 function Rectangle(width, height){ 21 Polygon.call(this, 2); 22 this.width = width; 23 this.height = height; 24 this.getArea = function(){ 25 return this.width * this.height; 26 }; 27 } 28 29 var rect = new Rectangle(5, 10); 30 console.log(rect.sides); //undefined 31 </script> 32 </body> 33 </html>
构造函数窃取结合使用原型链可以解决这个问题
这样一来,一个Rectangle实例也同时是一个Polygon实例,所以Polygon.call()会照原意执行,最终为Rectangle实例添加sides属性。
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <title>作用域安全的构造函数</title> 6 </head> 7 <body> 8 <script type="text/javascript"> 9 function Polygon(sides){ 10 if (this instanceof Polygon) { 11 this.sides = sides; 12 this.getArea = function(){ 13 return 0; 14 }; 15 } else { 16 return new Polygon(sides); 17 } 18 } 19 function Rectangle(width, height){ 20 Polygon.call(this, 2); 21 this.width = width; 22 this.height = height; 23 this.getArea = function(){ 24 return this.width * this.height; 25 }; 26 } 27 Rectangle.prototype=new Polygon();//实现继承 28 var rect = new Rectangle(5, 10); 29 console.log(rect.sides); //2 30 </script> 31 </body> 32 </html>
推荐作用域安全的构造函数作为最佳实践。
惰性载入函数
惰性载入表示函数执行的分支仅会发生一次。
有两种实现惰性载入的方式,第一种就是在函数被调用时再处理函数。在第一次调用的过程中,该函数会被覆盖为另一个按合适方式执行的函数,这样任何对原函数的调用都不用再经过执行的分支了。
在这个惰性载入的createXHR()中,if语句的每一个分支都会为createXHR变量赋值,有效覆盖了原有的函数。最后一步便是调用新赋的函数。下一次调用createXHR()的时候,就会直接调用被分配的函数,这样就不需要再次执行if语句了。这就是惰性载入的第一种核心思想。
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 6 <title>惰性载入函数</title> 7 </head> 8 <body> 9 <script type="text/javascript"> 10 function createXHR(){ 11 if (typeof XMLHttpRequest != "undefined"){ 12 createXHR = function(){ 13 return new XMLHttpRequest(); 14 }; 15 } else if (typeof ActiveXObject != "undefined"){ 16 createXHR = function(){ 17 if (typeof arguments.callee.activeXString != "string"){ 18 var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", 19 "MSXML2.XMLHttp"], 20 i, len; 21 22 for (i=0,len=versions.length; i < len; i++){ 23 try { 24 new ActiveXObject(versions[i]); 25 arguments.callee.activeXString = versions[i]; 26 } catch (ex){ 27 //skip 28 } 29 } 30 } 31 return new ActiveXObject(arguments.callee.activeXString); 32 }; 33 } else { 34 createXHR = function(){ 35 throw new Error("No XHR object available."); 36 }; 37 } 38 return createXHR(); 39 } 40 var xhr1 = createXHR(); 41 var xhr2 = createXHR(); 42 </script> 43 </body> 44 </html>
第二种实现方式:在声明函数时就指定适当的函数。
这样,第一次调用函数时就不会损失性能了,而在代码首次加载时会损失一点性能(因为首次加载需要经过每一个if分支来确定使用哪一个函数声明更好)。
这个例子的技巧:创建了一个自执行的匿名函数,用以确定应该使用哪一个函数实现。每个分支都返回正确的函数定义,以便立即将其赋值给createXHR()。这样我们在第一次调用createXHR()的时候,就直接使用的是最佳的函数声明,不会再走if分支判断了。
这种惰性载入函数的优点是:只在执行分支代码时牺牲一点性能。
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 6 <title>惰性载入函数</title> 7 </head> 8 <body> 9 <script type="text/javascript"> 10 var createXHR = (function(){ 11 if (typeof XMLHttpRequest != "undefined"){ 12 return function(){ 13 return new XMLHttpRequest(); 14 }; 15 } else if (typeof ActiveXObject != "undefined"){ 16 return function(){ 17 if (typeof arguments.callee.activeXString != "string"){ 18 var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", 19 "MSXML2.XMLHttp"], 20 i, len; 21 22 for (i=0,len=versions.length; i < len; i++){ 23 try { 24 new ActiveXObject(versions[i]); 25 arguments.callee.activeXString = versions[i]; 26 break; 27 } catch (ex){ 28 //skip 29 } 30 } 31 } 32 return new ActiveXObject(arguments.callee.activeXString); 33 }; 34 } else { 35 return function(){ 36 throw new Error("No XHR object available."); 37 }; 38 } 39 })(); 40 var xhr1 = createXHR(); 41 var xhr2 = createXHR(); 42 </script> 43 </body> 44 </html>
函数绑定
函数绑定要创建一个函数,可以在特定的this环境中以指定参数调用另一个函数。该技巧常常和回调函数与事件处理程序一起使用,以便在将函数作为变量传递的同时保留代码执行环境。
js库实现了一个可以将函数绑定到指定环境的函数–bind()
bind()函数接收一个函数和一个环境,并返回一个在给定环境中调用给定函数的函数,并且将所有参数原封不动传递过去。
在bind()函数中创建了一个闭包,闭包使用apply()调用传入的函数,并给apply()传递context对象和参数。
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <title>函数绑定</title> 6 </head> 7 <body> 8 <input type="button" id="my-btn" value="Click Me" /> 9 <script type="text/javascript" src="EventUtil.js"></script> 10 <script type="text/javascript"> 11 function bind(fn, context){//接收一个函数和一个环境 12 return function(){ 13 return fn.apply(context, arguments); 14 }; 15 } 16 var handler = { 17 message: "Event handled", 18 handleClick: function(event){ 19 console.log(this.message + ":" + event.type);//Event handled:click 20 } 21 }; 22 var btn = document.getElementById("my-btn"); 23 EventUtil.addHandler(btn, "click", bind(handler.handleClick, handler)); 24 </script> 25 </body> 26 </html>
函数绑定要创建一个函数,可以在特定的this环境中以指定参数调用另一个函数。该技巧常常和回调函数与事件处理程序一起使用,以便在将函数作为变量传递的同时保留代码执行环境。
下面的例子将对象handler的handleClick方法分配为按钮的事件处理程序。当按下按钮时,就应该调用该函数,显示一个警告框。虽然貌似警告框应该显示Event handled,然而实际上显示undefined。这是因为没有保存handler.handleClick()的执行环境,所以this指向了DOM按钮而不是handler对象。
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <title>函数绑定</title> 6 </head> 7 <body> 8 <input type="button" id="my-btn" value="Click Me" /> 9 <script type="text/javascript" src="EventUtil.js"></script> 10 <script type="text/javascript"> 11 var handler = { 12 message: "Event handled", 13 handleClick: function(event){ 14 //console.log(this);//<input id="my-btn" type="button" value="Click Me"> 15 console.log(this.message);//undefined 16 } 17 }; 18 var btn = document.getElementById("my-btn"); 19 EventUtil.addHandler(btn, "click", handler.handleClick); 20 </script> 21 </body> 22 </html>
函数绑定要创建一个函数,可以在特定的this环境中以指定参数调用另一个函数。该技巧常常和回调函数与事件处理程序一起使用,以便在将函数作为变量传递的同时保留代码执行环境。
下面的例子将对象handler的handleClick方法分配为按钮的事件处理程序。当按下按钮时,就应该调用该函数,显示一个警告框。虽然貌似警告框应该显示Event handled,然而实际上显示undefined。这是因为没有保存handler.handleClick()的执行环境,所以this指向了DOM按钮而不是handler对象。
这里在onclick事件处理程序中使用了一个闭包直接调用handler.handleClick()。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>函数绑定</title> </head> <body> <input type="button" id="my-btn" value="Click Me" /> <script type="text/javascript" src="EventUtil.js"></script> <script type="text/javascript"> var handler = { message: "Event handled", handleClick: function(event){ //console.log(this);// Object { message="Event handled", handleClick=function()} console.log(this.message);//Event handled } }; var btn = document.getElementById("my-btn"); EventUtil.addHandler(btn, "click", function(event){ handler.handleClick(event); }); </script> </body> </html>
ECMAScript5为所有函数定义了一个原生的bind()方法,进一步简单了操作。
不管原生的bind方法还是自定义的bind方法,都需要传入作为this值的对象
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <title>bind函数绑定</title> 6 </head> 7 <body> 8 <input type="button" id="my-btn" value="Click Me" /> 9 <script type="text/javascript" src="EventUtil.js"></script> 10 <script type="text/javascript"> 11 var handler = { 12 message: "Event handled", 13 handleClick: function(event){ 14 console.log(this.message + ":" + event.type); 15 } 16 }; 17 var btn = document.getElementById("my-btn"); 18 EventUtil.addHandler(btn, "click", handler.handleClick.bind(handler)); 19 </script> 20 </body> 21 </html> 22 23 函数柯里化 24 柯里化是一种允许使用部分函数参数构造函数的方式。也就是意味着,你在调用一个函数时,可以传入需要的全部参数并获得返回结果,也可以传入部分参数并的得到一个返回的函数,这个返回的函数需要传入的就是其余的参数。 25 Demo: 26 27 <script type="text/javascript"> 28 function curry(fn){ 29 var args = Array.prototype.slice.call(arguments,1); 30 return function(){//这里使用了闭包 31 var innerArgs = Array.prototype.slice.call(arguments); 32 var finalArgs = args.concat(innerArgs); 33 return fn.apply(null,finalArgs); 34 } 35 } 36 function add(num1,num2,num3){ 37 return num1+num2+num3; 38 } 39 var curriedAdd = curry(add,4); 40 var res = curriedAdd(5,6); 41 console.log(res);//15 42 </script>
上面这个例子中,add函数就是要柯里化的函数,其只传入了一个参数4,这个函数返回了一个柯里化的函数,这个柯里化的函数接收剩余的参数。
个人理解:* 这里curriedAdd就是函数add第一个参数为4的柯里化版本。*
函数柯里化–用于创建已经设置好了一个或多个参数的函数。其基本方法与函数绑定一样:使用一个闭包返回一个函数。两者区别在于:函数柯里化在函数被调用时,返回的函数还需要传入参数。
创建方法:调用另一个函数并为它传入要柯里化的函数和必要参数
Demo1
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <title>函数柯里化</title> 6 </head> 7 <body> 8 <script type="text/javascript"> 9 function curry(fn){ 10 //这里在arguments对象上调用了slice()方法,并传入参数1表示被返回的数组包含从第二个参数开始的所有参数 11 var args = Array.prototype.slice.call(arguments, 1);//slice() 方法可从已有的数组中返回选定的元素。 12 return function(){ 13 var innerArgs = Array.prototype.slice.call(arguments),//innerArgs表示内部函数的参数数组 14 finalArgs = args.concat(innerArgs);//将外部函数参数数组和内部函数参数数组进行连接 15 //concat()连接两个或更多的数组,并返回结果。 16 return fn.apply(null, finalArgs); 17 //这里的null表示没有考虑fn函数的执行环境 18 //这样一来this指向Global,而在浏览器环境下,Global就是window 19 }; 20 } 21 22 function add(num1, num2){//求和函数 23 return num1 + num2; 24 } 25 //curry()函数的第一个参数是要柯里化的函数,其他参数是要传入的值 26 var curriedAdd = curry(add, 5);//这里的5是外部函数参数,3是内部函数参数 27 alert(curriedAdd(3)); //8 28 29 var curriedAdd2 = curry(add, 5, 12);//柯里化的add函数 30 alert(curriedAdd2()); //17 31 32 </script> 33 </body> 34 </html>
Demo2
函数柯里化还常常作为函数绑定的一部分包含在其中,构造出更为复杂的bind函数。
这里bind同时接受函数和一个object对象。表示给被绑定的函数的参数是从第三个开始的。
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <title>函数柯里化</title> 6 </head> 7 <body> 8 <input type="button" id="my-btn" value="Click Me"> 9 <script type="text/javascript" src="../EventUtil.js"></script> 10 <script type="text/javascript"> 11 function bind(fn, context){//fn=handler.handleClick context=handler 12 var args = Array.prototype.slice.call(arguments, 2);//获取到"my-btn" 13 return function(){ 14 var innerArgs = Array.prototype.slice.call(arguments), 15 finalArgs = args.concat(innerArgs); 16 return fn.apply(context, finalArgs);//handler.handleClick.apply(handler, "my-btn"); 17 }; 18 } 19 20 var handler = { 21 message: "Event handled", 22 handleClick: function(name, event){//name是要处理的元素的名字 23 //event就是event对象 24 console.log(this.message + ":" + name + ":" + event.type);//Event handled:my-btn:click 25 } 26 }; 27 28 var btn = document.getElementById("my-btn"); 29 EventUtil.addHandler(btn, "click", bind(handler.handleClick, handler, "my-btn")); 30 </script> 31 </body> 32 </html> 33 34 Demo3 35 36 <!DOCTYPE html> 37 <html> 38 <head> 39 <meta charset="utf-8"> 40 <title>函数柯里化</title> 41 </head> 42 <body> 43 <input type="button" id="my-btn" value="Click Me"> 44 <script type="text/javascript" src="../EventUtil.js"></script> 45 <script type="text/javascript"> 46 var handler = { 47 message: "Event handled", 48 handleClick: function(name, event){ 49 console.log(this.message + ":" + name + ":" + event.type); 50 } 51 }; 52 var btn = document.getElementById("my-btn"); 53 EventUtil.addHandler(btn, "click", handler.handleClick.bind(handler, "my-btn")); 54 //handler.handleClick.bind(handler, "my-btn")这样绑定指handler.handleClick函数中的this指向handler对象 55 //bind()方法也实现了函数柯里化,只要在this的值之后再传入另一个参数即可 56 </script> 57 </body> 58 </html> 59 60 防纂改对象 61 不可扩展对象 62 Object.preventExtensions()方法用来阻止给对象添加属性和方法。但是已有的成员丝毫不受影响,你仍然可以修改和删除已有的成员。 63 64 <script type="text/javascript"> 65 var person = { name: "Nicholas" }; 66 Object.preventExtensions(person); 67 68 person.age = 29; 69 console.log(person.age);//undefined 70 </script> 71 72 <script type="text/javascript"> 73 /* 74 Object.preventExtensions()用来阻止给对象添加属性和方法 75 Object.isExtensible()方法用来判断元素是否可以扩展 76 */ 77 var person = { name: "Nicholas" }; 78 console.log(Object.isExtensible(person)); //true 79 80 Object.preventExtensions(person); 81 console.log(Object.isExtensible(person)); //false 82 83 person.age = 29; 84 console.log(person.age);//undefined 85 </script> 86 87 密封的对象 88 密封对象不可扩展,而且已有成员的[[Configurable]]特性将被设置为false。这就意味着不能删除属性和方法,但属性值是可以修改的。 89 90 <script type="text/javascript"> 91 /* 92 Object.seal()将对象密封,不能给对象添加和删除属性和方法 93 */ 94 var person = { name: "Nicholas" }; 95 Object.seal(person); 96 person.age = 29; 97 console.log(person.age); //undefined 98 //表示可以修改属性值 99 person.name = "liss"; 100 console.log(person.name);//liss 101 delete person.name; 102 console.log(person.name); //"Nicholas" 103 </script> 104 105 <script type="text/javascript"> 106 var person = { name: "Nicholas" }; 107 console.log(Object.isExtensible(person)); //true 返回true表示对象可以扩展 108 console.log(Object.isSealed(person)); //false 返回false表示对象没有密封 109 110 Object.seal(person); 111 //密封的对象不可扩展,所以这里返回false 112 console.log(Object.isExtensible(person)); //false 113 console.log(Object.isSealed(person)); //true 对象被密封 114 115 person.age = 29; 116 console.log(person.age);//undefined" 117 </script> 118 119 冻结的对象 120 <script type="text/javascript"> 121 /* 122 Object.freeze()方法是冻结对象,冻结的对象既不能扩展,同时也是密封的,而且对象的[[writeable]]特性也被设置为false 123 */ 124 var person = { name: "Nicholas" }; 125 Object.freeze(person); 126 127 person.age = 29;//不可以扩展 128 console.log(person.age); //undefined 129 130 delete person.name;//不可以删除 131 console.log(person.name); //"Nicholas" 132 133 person.name = "Greg";//因为writeable]]特性被设置为false的原因,不能被修改 134 console.log(person.name); //"Nicholas" 135 </script> 136 137 <script type="text/javascript"> 138 var person = { name: "Nicholas" }; 139 console.log(Object.isExtensible(person)); //true 140 console.log(Object.isSealed(person)); //false 141 console.log(Object.isFrozen(person)); //false Object.isFrozen()用来判断对象是否被冻结 142 143 Object.freeze(person); 144 console.log(Object.isExtensible(person)); //false 145 console.log(Object.isSealed(person)); //true 146 console.log(Object.isFrozen(person)); //true 147 148 person.age = 29; 149 console.log(person.age);//undefined 150 </script>
高级定时器
js是运行于单线程的环境中的,定时器仅仅只是计划代码在未来的某个时间执行。执行时机是不能保证的,因为在页面的生命周期中,不同事件可能有其他代码在控制js进程。在页面下载完后的代码运行、事件处理程序、Ajax回调函数都必须使用同样的线程来执行。实际上,浏览器负责进行排序,指派某段代码在某个时间点运行的优先级。
当某个按钮被按下,它的事件处理程序代码就会被添加到队列中,并在下一个可能的时间里执行。当接收到某个Ajax响应时,回调函数的代码会被添加到队列。在js中没有任何代码时立刻执行的,但是一旦进程空闲则尽快执行。
定时器对队列的工作方式是:当特定时间过去后将代码插入。注意,给队列添加代码并不意味着对它立刻执行,而只能表示它会尽快执行。例如:设定一个150ms后执行的定时器不代表到了150ms代码就立刻执行,它表示代码会在150ms后被加入到队列中。如果在这个时间点,队列中没有其他东西,那么这段代码就会被执行,表面上看上去就好像代码就在精确的时间点上执行了。其他情况,代码可能明显等待更长时间才执行。
Demo1:
为了避免setInterval()的重复定时器的缺点,可以采用链式setTimeout()方式
调用setTimeout(),每次函数执行的时候都会创建一个新的定时器。第二个setTimeout()调用使用了arguments.callee来获取对当前执行的函数的引用,并为其设置另外一个定时器。这样做的好处:在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔。而且,它可以保证在下一次定时器代码执行之前,至少要等待指定的间隔,避免了连续的运行。这个模式主要用于重复定时器。
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 6 <title>重复的定时器</title> 7 </head> 8 <body> 9 <div id="myDiv" style="position:absolute;width:100px;height:100px;left:0px;top:10px;background:red;"></div> 10 <script type="text/javascript"> 11 setTimeout(function() 12 { 13 var div = document.getElementById("myDiv"), 14 left = parseInt(div.style.left) + 5; 15 div.style.left = left + "px"; 16 17 if (left < 200){ 18 setTimeout(arguments.callee, 50); 19 } 20 21 }, 50); 22 </script> 23 </body> 24 </html>
数组分块技术
数组分块技术基本的思路:为要处理的项目创建一个队列,然后使用定时器取出下一个要处理的项目进行处理,接着再设置另一个定时器。
数组分块的重要性在于它可以将多个项目的处理在执行队列上分开,在每个项目处理之后,给予其他的浏览器处理机会运行,这样就可能避免长时间运行脚本的错误。
data.concat():当不传递任何参数调用数组的concat()方法时,将返回和原来数组中项目一样的数组。
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 6 <title>数组分块技术</title> 7 </head> 8 <body> 9 <div id="myDiv" style="background:red;"></div> 10 <script type="text/javascript"> 11 var data = [12,123,1234,453,436,23,23,5,4123,45,346,5634,2234,345,342]; 12 function chunk(array, process, context){ 13 //三个参数:要处理的项目的数组,用于处理项目的函数,可选的运行该函数的环境 14 setTimeout(function(){ 15 var item = array.shift();//获取队列中下一个要处理的项目 16 process.call(context, item); 17 18 if (array.length > 0){ 19 setTimeout(arguments.callee, 100); 20 } 21 }, 100); 22 } 23 function printValue(item){ 24 var div = document.getElementById("myDiv"); 25 div.innerHTML += item + "<br>"; 26 } 27 chunk(data, printValue); 28 </script> 29 </body> 30 </html>
函数节流
DOM操作比起非DOM交互需要更多的内存和CPU时间,连续尝试进行过多的DOM相关操作可能会导致浏览器挂起,有时候甚至会崩溃。
如果在程序中使用了onresize事件处理程序,当调整浏览器大小的时候,该事件会连续触发。如果在该事件处理程序内部进行了相关DOM操作,其高频率的更改可能会导致浏览器崩溃。为了绕开这个问题,我们可以考虑使用定时器对该函数进行节流。
函数节流背后的基本思想是:某些代码不可以在没有间断的情况下连续重复执行。第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除之前的定时器并设置另一个。如果前一个定时器已经执行过了,这个操作就没有任何意义。然而,如果前一个定时器尚未执行,其实就是将其替换为一个新的定时器。目的是在只有在执行函数的请求停止了一段时间之后才执行。
我们使用throttle()函数来实现定时器的设置和清除。
throttle()函数接收两个参数:要执行的函数以及在哪个作用域中执行。在函数中先清除之前设置的任何定时器。定时器ID是存储在函数的tId属性中的。
只要代码是周期性执行的,都应该使用节流,但是你不能控制请求执行的速率。
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 6 <title>函数节流</title> 7 </head> 8 <body> 9 <div id="myDiv" style="background:red;"></div> 10 <script type="text/javascript"> 11 function throttle(method, scope) { 12 //下一次执行前,先清除上一次的定时器,控制处理的频率 13 clearTimeout(method.tId); 14 method.tId= setTimeout(function(){ 15 method.call(scope); 16 }, 100); 17 } 18 function resizeDiv(){ 19 var div = document.getElementById("myDiv"); 20 div.style.height = div.offsetWidth + "px"; 21 } 22 window.onresize = function(){ 23 //调用节流函数进行处理频率的控制 24 throttle(resizeDiv); 25 }; 26 </script> 27 </body> 28 </html>
节流在resize事件中是最常用的。如果你基于该事件来改变页面布局的话,最好控制处理的频率,以确保浏览器不会在极短的时间内进行过多的计算。
在上面的例子中有两个问题可能会导致浏览器运行缓慢。首先,要计算offsetWidth属性,如果该元素或者页面上其他元素有非常复杂的CSS样式,那么这个过程将会很复杂。其次,设置某个元素的高度需要对页面进行回流来令改动生效。如果页面有很多元素同时应用了相当数量的CSS的话,这又需要很多计算。
自定义事件
事件是一种叫做观察者的设计模式,这是一种创建松散耦合代码的技术,对象可以发布事件,用来表示在该对象生命周期中某个时刻到了,然后其他对象可以观察该对象,等待这些时刻到来并通过运行代码来响应。
观察者模式由两类对象组成:主体和观察者。主体负责发布事件,同时观察者通过订阅这些事件来观察主体。该模式的一个关键概念是主体并不知道观察者的任何事情,也就是说它可以独自存在并正常运作即使观察者不存在。
Demo1
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 6 <title>自定义事件</title> 7 </head> 8 <body> 9 <div id="myDiv" style="background:red;"></div> 10 <script type="text/javascript" src="EventTarget.js"></script> 11 <script type="text/javascript"> 12 function handleMessage(event){ 13 console.log("Message received: " + event.message);//Hello world! 14 } 15 //创建一个新对象 16 var target = new EventTarget(); 17 //添加一个事件处理程序 18 target.addHandler("message", handleMessage); 19 //触发事件 20 target.fire({ type: "message", message: "Hello world!"}); 21 //移除事件处理程序 22 target.removeHandler("message", handleMessage); 23 //再次触发,但不会显示任何警告框 24 target.fire({ type: "message", message: "Hello world!"}); 25 </script> 26 </body> 27 </html> 28 29 EventTarget.js 30 31 function EventTarget(){//构造函数 32 this.handlers = {};//该属性用来存储事件处理程序 33 } 34 35 EventTarget.prototype = {//原型对象 36 constructor: EventTarget, 37 38 addHandler: function(type, handler){//该方法用于注册给定类型事件的事件处理程序 39 //该方法接受两个参数:事件类型和用于处理该事件的函数。当调用该方法时,会进行一次检查, 40 //看看handlers属性中是否已经存在一个针对该事件类型的数组;如果没有,则创建一个新的。 41 //然后使用push()方法将该处理程序添加到数组的末尾 42 if (typeof this.handlers[type] == "undefined"){ 43 this.handlers[type] = []; 44 } 45 this.handlers[type].push(handler); 46 }, 47 48 fire: function(event){//该方法用于触发一个事件 49 /*该方法接受一个单独的参数,是一个至少包含type属性的对象。fire()方法先给event对象设置 50 一个target属性,如果它尚未被指定的话。然后它就查找对应事件类型的一组处理程序,调用各个函数, 51 并给出event对象。 52 */ 53 if (!event.target){ 54 event.target = this; 55 } 56 if (this.handlers[event.type] instanceof Array){ 57 var handlers = this.handlers[event.type]; 58 for (var i=0, len=handlers.length; i < len; i++){ 59 handlers[i](event); 60 } 61 } 62 }, 63 64 removeHandler: function(type, handler){//该方法用于注销某个事件类型的事件处理程序 65 if (this.handlers[type] instanceof Array){ 66 var handlers = this.handlers[type]; 67 for (var i=0, len=handlers.length; i < len; i++){ 68 if (handlers[i] === handler){ 69 //这个方法搜索事件处理程序的数组找到要删除的处理程序的位置。如果找到了, 70 //则使用break操作符退出for循环。然后使用splice()方法将该项目从数组中删除 71 break; 72 } 73 } 74 handlers.splice(i, 1); 75 } 76 } 77 };
Person类型使用寄生组合继承方法来继承EventTarget
Demo2
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 6 <title>自定义事件</title> 7 </head> 8 <body> 9 <div id="myDiv" style="background:red;"></div> 10 <script type="text/javascript"> 11 function object(o){ 12 function F(){} 13 F.prototype = o; 14 return new F(); 15 } 16 17 function inheritPrototype(subType, superType){ 18 var prototype = object(superType.prototype);//创建对象 19 prototype.constructor = subType;//增强对象 20 subType.prototype = prototype; //指定对象 21 } 22 23 function Person(name, age){ 24 EventTarget.call(this); 25 this.name = name; 26 this.age = age; 27 } 28 29 inheritPrototype(Person,EventTarget); 30 Person.prototype.say = function(message){ 31 this.fire({type: "message", message: message}); 32 }; 33 function handleMessage(event){ 34 console.log(event.target.name + " says: " + event.message); 35 } 36 var person = new Person("Nicholas", 29); 37 person.addHandler("message", handleMessage); 38 person.say("Hi there."); 39 </script> 40 </body> 41 </html>
本文来自学习小花,作者:aixuexi666888,转载请注明原文链接:https://www.cnblogs.com/aixuexi666888/p/15509702.html