js之高階技巧
安全的型別檢測
js內建的型別檢測方法並不安全。
①typeOf 不能用於檢測array只能返回object,不能檢測null只能返回object,但是出乎意料的是可以檢測symbol、function和undefined。型別返回值都是字串、而且都是小寫打頭。
②instanceOf是用來判斷是不是某個東西的例項物件的,比如一道經典的題:
var isArray = value instanceof Array;
要求value與Array要在同一個作用域,因為Array是window的屬性,所以肯定是全域性作用域,如果這個網頁用到了iframe,value是在頁面中另一個iframe中定義的陣列的話,以上程式碼會返回false。
所以我們會使用Object.prototype.toString.call(value) 會返回[object value的型別。即使是RexExp也可以使用。
作用域安全的建構函式
當我們不寫new時,是把建構函式當作普通函式呼叫,這個時候建構函式內的this指向widow。
為了避免這種情況的發生,我們要在建構函式內先進行一個判斷,判斷this是否為建構函式的例項物件。
function Polygon(sides){ if(this instanceof Polygon){ this.sides = sides; this.getArea = function(){ return 0; } }else{ return new Polygon(); } }
當然這種寫法也有它的弊端,與call()函式一起使用時會出問題,所以要結合原型鏈繼承一起使用,形成我們的偽經典繼承。
function Polygon(sides){ if(this instanceof Polygon){ this.sides = sides; this.getArea = function(){ return 0; } }else{ return new Polygon(); } } function Rectangle(width,height){ Polygon.call(this,2); this.width = width; this.height = height; this.getArea = function(){ return this.width*this.height; }; } Rectangle.prototype = new Polygon();//沒有這句最後就console不出2 var rect = new Rectangle(5,10); console.log(rect.sides);
函式繫結
函式可以在特定的this環境中以指定引數呼叫另一個函式。
可以通過建立閉包保持一個this,也可以通過bind、call、apply改變this指向。
var handler = {
message: "Event handled",
handleClick: function(){
console.log(this);
}
};
var btn = document.getElementById("my-btn");
btn.addEventListener("click",function(){
handler.handleClick();//此時this指向handler,這是一個閉包
},false);
btn.addEventListener("click",handler.handleClick,false);
//此時this指向btn
btn.addEventListener("click",handler.handleClick.bind(handler),false);
//此時this指向handler,bind返回一個更改了this的函式
常考的一道題是手寫個bind:
function bind(fn,context){
return function(){
return fn.call(context);
}
};
function f1(){
return this;
}
console.log(f1());//Window
console.log(bind(f1,[1,2,3])());//[1,2,3]
函式柯里化
基本方法和函式繫結是一樣的:使用一個閉包返回一個函式。
function curry(fn){
var args = Array.prototype.slice.call(arguments,1);
//arguments的第0位是fn,即要進行柯里化的引數
return function(){
var innerArgs = Array.prototype.slice.call(arguments);//內部函式的陣列
var finalArgs = args.concat(innerArgs);
return fn.apply(null,finalArgs);
// return fn.call(null,args[0],innerArgs[0]);也可以
};
};
function add(num1,num2){
return num1+num2;
}
var curriedAdd = curry(add,5);//args=5
console.log(curriedAdd(3));//innerArgs=3
記得上一個“函式繫結”時我們寫的bind嗎?並沒有寫關於引數的問題(因為不需要寫),現在函式柯里化必須要對內外函式的引數進行處理了。
var handler = {
message: "Event handled",
handleClick: function(name,event){
console.log(this.message+":"+name+":"+event.type);
}
};
//可以用於函式柯里化和函式繫結的更牛掰的bind
function bind(fn,context){
var args = Array.prototype.slice.call(arguments,2);//外部函式
return function(){
//內部函式
var innerArgs = Array.prototype.slice.call(arguments,0);
var finalArgs = args.concat(innerArgs);
return fn.apply(context,finalArgs);
}
}
var btn = document.getElementById("my-btn");
btn.addEventListener("click",bind(handler.handleClick,handler,"my-btn"),false);//Event handled:my-btn:click
防篡改物件
有時候我們寫了一個js庫,裡面有一些核心物件,我們希望在開發過程中這個核心物件不被修改,這時候就要防止該物件被篡改。
①不可篡改
呼叫Object.preventExtensions(object)方法就不能給object新增新屬性和方法了,但是已有的成員還是可以修改或刪除的。
使用Object.isExtensible()方法確定物件是否可以擴充套件。
②密封
呼叫Object.isSealed()方法來密封物件,不能刪除和新增屬性和方法,但是可以修改屬性和方法。使用 Object.isSealed()方法確定物件是否被密封。用 Object.isExtensible()檢測密封的物件也會返回 false,表明被密封的物件不可擴充套件。
③凍結
呼叫Object.freeze()方法來凍結物件,不能刪除、新增和修改屬性和方法。使用Object.isFrozen()方法檢測凍結物件。用 Object.isExtensible()和 Object.isSealed()檢測凍結物件將分別返回 false 和 true,因為凍結=不可篡改+密封。
以上可以看出,防止篡改物件的嚴格優先順序從高到低依次是 :凍結>密封>擴充套件
自定義事件
事件是一種叫 觀察者 的設計模式,話說我第一次聽說這個詞還是在學vue的時候,這裡我也不展開了,學得太糙回來再補。
它是一種建立鬆散耦合程式碼的技術,定義物件之間一對多的依賴關係,一個主體改變狀態,其他所有觀察者通過訂閱事件都能獲得通知,就好比你寫作業的時候爸爸媽媽姥姥姥爺爺爺奶奶全盯著你!
例子:有一天你登陸了一個網站,網站的頭會顯示你的使用者名稱和頭像,下面的顯示也是與你的賬號有關,旁邊可能還有購物車。我們通過獲取ajax返回的資訊,觸發多個事件,而這些事件統統放到一個數組中。
我壓縮了一下大紅書上的程式碼:
function EventTarget(){
this.handlers = {};
};
EventTarget.prototype = {
constructor:EventTarget,
addHandler: function(type,handler){//新增事件處理程式
if(!this.handlers[type]){
this.handlers[type] = new Array;
}
this.handlers[type].push(handler);
},
fire: function(event){//觸發
if(this.handlers[event.type]){
for(var i=0; i<this.handlers[event.type].length; i++){
this.handlers[event.type][i](event);
}
}
},
removeHandler: function(type,handler){//刪除事件處理程式
if(this.handlers[type]){
for(var i=0; i<this.handlers[type].length; i++){
if(this.handlers[type][i] === handler){
break;
}
};
this.handlers[type].splice(i,1);
}
}
}
其中EventTarget是一個事件管理器,handles用來儲存事件處理程式,每一項都是一個事件type,裡面的內容是這個事件type所對應的多個事件處理程式。
在網上看到一道筆試題,非常經典非常好!強烈推薦大家啥都不看也要做做這道題,查缺補漏~
[附加題] 請實現下面的自定義事件 Event 物件的介面,功能見註釋(測試1)
該 Event 物件的介面需要能被其他物件拓展複用(測試2)
// 測試1
Event.on('test', function (result) {
console.log(result);
});
Event.on('test', function () {
console.log('test');
});
Event.emit('test', 'hello world'); // 輸出 'hello world' 和 'test'
// 測試2
var person1 = {};
var person2 = {};
Object.assign(person1, Event);
Object.assign(person2, Event);
person1.on('call1', function () {
console.log('person1');
});
person2.on('call2', function () {
console.log('person2');
});
person1.emit('call1'); // 輸出 'person1'
person1.emit('call2'); // 沒有輸出
person2.emit('call1'); // 沒有輸出
person2.emit('call2'); // 輸出 'person2'
var Event = {
// 通過on介面監聽事件eventName
// 如果事件eventName被觸發,則執行callback回撥函式
on: function (eventName, callback) {
//你的程式碼
},
// 觸發事件 eventName
emit: function (eventName) {
//你的程式碼
}
};
首先完成第一個測試題需要用到我們的觀察者模式啦~啥也不說直接上程式碼!
var Event = {
// 通過on介面監聽事件eventName
// 如果事件eventName被觸發,則執行callback回撥函式
on: function (eventName, callback) {
if(!this.handlers){
this.handles={};
}
if(!this.handlers[eventName]){
this.handlers[eventName] = new Array();
};
this.handlers[eventName].push(callback);
},
// 觸發事件 eventName
emit: function (eventName) {
if(this.handlers[eventName]){
for(var i=0; i<this.handlers[eventName].length; i++){
this.handlers[eventName][i](arguments[1]);
}
};
}
};
當然可以輕鬆解決測試1,但測試2顯然不是這麼回事了,因為Object.assign是一種淺拷貝,也就是隻拷貝到物件的地址而不是地址中的內容,所以一榮俱榮一損俱損。
這時候用到的知識是學“物件”那一章的設定物件屬性,不知道大家想沒想起來,我反正是隻記得這麼個事情卻不太會使,導致這道題雖然知道坑在哪但沒想到要設定那裡。
var Event = {
// 通過on介面監聽事件eventName
// 如果事件eventName被觸發,則執行callback回撥函式
on: function (eventName, callback) {
if(!this.handlers){
//this.handles={};
Object.defineProperty(this, "handlers", {
value: {},
enumerable: false,
configurable: true,
writable: true
})
}
if(!this.handlers[eventName]){
this.handlers[eventName] = new Array();
};
this.handlers[eventName].push(callback);
},
// 觸發事件 eventName
emit: function (eventName) {
if(this.handlers[eventName]){
for(var i=0; i<this.handlers[eventName].length; i++){
this.handlers[eventName][i](arguments[1]);
}
};
}
};
Object.defineProperty()方法需要三個引數,分別是:屬性所在物件,這裡是Event本身;屬性的名字,這裡是handlers,注意必須加引號;一個描述符物件,描述符包括configurable、enumerable、writable 和 value。所以我們會這樣設定:讓handlers為不可列舉的enumerable:false,由於Object.assign()對不可列舉的屬性沒有辦法,於是person1和person2呼叫他的時候生成的handlers就不是同一個了。
我們再來詳細說說這描述符物件:
configurable : false 表示不能刪除屬性,而且一旦設定成false就沒法改成true了;
writable : false 表示不可修改
enumerable : false 表示不可列舉,不能通過for-in遍歷得到。
拖放
本單元最後一部分內容,也是我認為最重要的一部分,涉及的東西很多。
在完成之前需要一段完善的過程,否則不能稱為一個好作品,程式碼亦是如此,不可能最開始就想的非常全面,伴隨著bug伴隨著越來越多的需求,程式碼也在不斷地完善。
①首先是實現簡單的拖拽,分為三個部分,當滑鼠按下的時候,滑鼠移動的時候,和滑鼠鬆開的時候。
滑鼠按下的時候要確認滑鼠的位置同你所移動的物體之間的距離diff;
滑鼠移動的時候將物體的位置設為 滑鼠位置-diff;
滑鼠鬆開的時候取消前兩步的監聽。
在這裡插一句話:mousedown->focus->mouseup->click的順序一定要記牢!
var myDiv = document.getElementById("myDiv"),
count = 0,
diffX = 0,
diffY = 0;
function mousedown(event) {
diffX = event.clientX - this.offsetLeft;
diffY = event.clientY - this.offsetTop;
count = 1;
}
function mousemove(event) {
if(count === 1){
this.style.left = event.clientX - diffX + "px";
this.style.top = event.clientY - diffY + "px";
}
}
myDiv.addEventListener("mousedown",mousedown,false);
myDiv.addEventListener("mousemove",mousemove,false);
myDiv.addEventListener("mouseup",function(event){
myDiv.removeEventListener("mousedown",mousedown(event));
myDiv.removeEventListener("mousemove",mousedown(event));
count = 0;
},false);
②如果我們想在這三步的時候分別實現不同的事件處理程式呢?結合之前的自定義事件非常好實現。
//自定義事件
function EventTarget(){
this.handlers = this.handlers||{};
this.addHandler = function(type,handler){
if(!this.handlers[type]){
this.handlers[type] = [];
}
this.handlers[type].push(handler);
};
this.fire = function(obj){
for(var i=0; i<this.handlers[obj.type].length; i++){
this.handlers[obj.type][i](obj);
}
};
this.removeHanlder = function(type,handler){
for(var i=0; i<this.handlers[type].length; i++){
if(this.handlers[type][i]===handler){
this.handlers[type].splice(i,1);
}
}
};
}
var dragdrop = new EventTarget();
var myDiv = document.getElementById("myDiv"),
count = 0,
diffX = 0,
diffY = 0;
function mousedown(event) {
diffX = event.clientX - this.offsetLeft;
diffY = event.clientY - this.offsetTop;
count = 1;
dragdrop.fire({type:"dragstart",target:myDiv,x:event.clientX,y:event.clientY});
}
function mousemove(event) {
if(count === 1){
this.style.left = event.clientX - diffX + "px";
this.style.top = event.clientY - diffY + "px";
dragdrop.fire({type:"drag",target:myDiv,x:event.clientX,y:event.clientY});
}
}
myDiv.addEventListener("mousedown",mousedown,false);
myDiv.addEventListener("mousemove",mousemove,false);
myDiv.addEventListener("mouseup",function(event){
myDiv.removeEventListener("mousedown",mousedown);
myDiv.removeEventListener("mousemove",mousemove);
count = 0;
dragdrop.fire({type:"dragend",target:myDiv,x:event.clientX,y:event.clientY});
},false);
//注意要加一個定時器,否則只能拖拽一次
setInterval(function(){
myDiv.addEventListener("mousedown",mousedown,false);
myDiv.addEventListener("mousemove",mousemove,false);
},300);
dragdrop.addHandler("dragstart",function(event){
var status = document.getElementById("status");
status.innerHTML = "Started dragging "+ event.target.id;
});
dragdrop.addHandler("drag",function(event){
var status = document.getElementById("status");
status.innerHTML += "<br/>Dragged " +event.target.id+" to(" +event.x+","+event.y+")";
});
dragdrop.addHandler("dragend",function(event){
var status = document.getElementById("status");
status.innerHTML += "<br/>Dropped " +event.target.id+" at(" +event.x+","+event.y+")";
});
定時器雖然能使我們無限次使用拖拽,但也會帶來卡頓等問題,所以真正應該使用的方法是定義一個物件,等滑鼠鬆開的時候將這個物件設為null而不是粗暴地將滑鼠按下和滑鼠移動的事件監聽取消。
③再上一步的基礎之上,我們採用模組化程式設計的方式。
function EventTarget(){
this.handlers = {};
};
EventTarget.prototype = {
constructor:EventTarget,
addHandler: function(type,handler){
if(!this.handlers[type]){
this.handlers[type] = new Array;
}
this.handlers[type].push(handler);
},
fire: function(event){
if(this.handlers[event.type]){
for(var i=0; i<this.handlers[event.type].length; i++){
this.handlers[event.type][i](event);
}
}
},
removeHandler: function(type,handler){
if(this.handlers[type]){
for(var i=0; i<this.handlers[type].length; i++){
if(this.handlers[type][i] === handler){
break;
}
};
this.handlers[type].splice(i,1);
}
}
}
var DragDrop = function(){
var dragdrop = new EventTarget(),
dragging = null,
diffX = 0,
diffY = 0;
function handleEvent(event){
event = event||window.event;
var target = event.target||event.srcElement;
//確定事件型別
switch(event.type){
case "mousedown":
if(target.className.indexOf("draggable")>-1){
dragging = target;
diffX = event.clientX - target.offsetLeft;
diffY = event.clientY - target.offsetTop;
//觸發自定義事件
dragdrop.fire({type:"dragstart",target:dragging,x:event.clientX,y:event.clientY});
}
break;
case "mousemove":
if(dragging !== null){
//指定位置
dragging.style.left = event.clientX-diffX +"px";
dragging.style.top = event.clientY-diffY +"px";
//觸發自定義事件
dragdrop.fire({type:"drag",target:dragging,x:event.clientX,y:event.clientY});
}
break;
case "mouseup":
dragdrop.fire({type:"dragend",target:dragging,x:event.clientX,y:event.clientY});
dragging = null;
break;
}
};
//公共介面
dragdrop.enable = function(){
document.addEventListener("mousedown",handleEvent);
document.addEventListener("mousemove",handleEvent);
document.addEventListener("mouseup",handleEvent);
};
dragdrop.disable = function(){
document.removeEventListener("mousedown",handleEvent);
document.removeEventListener("mousemove",handleEvent);
document.removeEventListener("mouseup",handleEvent);
};
return dragdrop;
}();
DragDrop.addHandler("dragstart",function(event){
var status = document.getElementById("status");
status.innerHTML = "Started dragging "+ event.target.id;
});
DragDrop.addHandler("drag",function(event){
var status = document.getElementById("status");
status.innerHTML += "<br/>Dragged " +event.target.id+" to(" +event.x+","+event.y+")";
});
DragDrop.addHandler("dragend",function(event){
var status = document.getElementById("status");
status.innerHTML += "<br/>Dropped " +event.target.id+" at(" +event.x+","+event.y+")";
});
DragDrop.enable();
在這裡我們定義了dragging表示我們要拖拽的物件,DragDrop是拖拽模組,採用自執行函式的形式。
發現模組化設計也是一個需要好好研究的東西呢~