安全的类型检测
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是拖拽模块,采用自执行函数的形式。
发现模块化设计也是一个需要好好研究的东西呢~