UI层的松耦合
1.将JavaScript从css中抽离出来;现在大部分已经不支持;
2. 将css从JavaScript中抽离出来。
不要直接在js内添加样式;如:
e.style.color="red";
可以使用更改类名的方式来动态的修改样式。如:e.className=”css”
javascript不应直接操作样式,以便保持和css的松耦合。
3.将JavaScript从Html中抽离。
不要再写<h2 onclick="login()">登陆账号</h2>
;这样的函数体了。这是2000年左右的写法。
4.将Html从JavaScript中抽离。
渲染html的时候,总会插入一些html结构;如
var myDiv=document.querySelector("#mydiv");
myDiv.innerHTML="<h2>登陆成功</h2>";
缺点:
1.增加了文本和结构性的负责度。先在dom树中查找,然后再页面的html中比较不同。如果发生bug追中bug就成一件艰难的事了。
2.相比修改JavaScript代码,修改标签通常不会引起太多错误。
解决的办法:
1.从服务器加载。
就是向服务器发起请求获取字符串。
2.简单客户端模板。
放入script中:
<script type="text/x-handlebars-template" id="myTemp">
<li><a href="#">我关注的品牌</a></li>
</script>
然后通过script标签的text属性来提取模板文本。
var myTemp=document.querySelector("#myTemp");
var tempText=myTemp.text;
//插入指定的位置
document.querySelector("#myList").innerHTML=tempText;
3.复杂的客户端模板
可以使用一些流行的模板引擎来实现,如artTemplate.js
避免使用全局变量
由于一些简单的书写错误,可能造成对全局变量的修改。如:
function getData() {
var foo=10;
color="red";
return color;
}
这里一不小心就把全局变量color
修改了。
同时每个全局变量,每次查找的时候都会影响性能,;
单全局变量方式 ##
依赖尽可能少的全局变量,即只创建一个全局变量。这个唯一全局对象名是独一无二 的,不会和内置API产生冲突,并将你所有的功能代码都挂载到这个全局对象上。因此每个可能的全局变量都成为你唯一全局对象的属性,从而不会创建多个全局变量。如:
var MainPerson={};
MainPerson.People=function (title) {
this.title=title;
this.logo="I am the SuperMan"
}
MainPerson.People.prototype.sayHi=function (move) {
var str="when in the " + this.title+" I will say"+this.logo;
str+=" "+move;
console.log(str);
}
var tom=new MainPerson.People("afternoon");
tom.sayHi("run")
MainPerson.jack=new MainPerson.People("morning");
MainPerson.jack.sayHi("go go go");
//when in the afternoon I will sayI am the SuperMan run
//when in the morning I will sayI am the SuperMan go go go
上面的例子我们可以看到只有一个全局变量MainPerson,其他的任何信息都挂载到这个对象上。很容易让整个团队的人为它添加属性,避免全局污染。
命名空间
在JavaScript中,你可以使用对象轻而易举的创建自己的命名空间。一般有两条约定:
var MyBooks={};
//表示一个新的命名空间
MyBooks.WestStory={};
1.每个文件中都创建新的全局对象来声明自己的命名空间。如上面的例子。
2.每个文件都需要给一个命名空间挂载对象。—-在这种情况下,你需要首先保证这个命名空间是已经存在的,这时全局对象非破坏性的处理命名空间的方式变得非常有用,如下面代码所示:
var YourGlobal={
namespace:function (ns) {
var parts=ns.split("."),
obj=this,
i,len;
for(i=0,len=parts.length;i<len;i++){
if(!obj[parts[i]]){ //新的命名空间
obj[parts[i]]={};
}
obj=obj[parts[i]]; //将已经的存在的绑定到YourGlobal上。
}
return obj;
},
}
//同时创建了"YourGlobal.MainPerson.People"和"YourGlobal.People";
//因为之前没有创建过他们,因此每个都是全新创建的。
YourGlobal.namespace("MainPerson.People");
console.log(YourGlobal.MainPerson.People);
console.log(YourGlobal);
结果:
现在你可以使用这个命名空间;
YourGlobal.MainPerson.People.name="jack";
console.log(YourGlobal.MainPerson.People); //{name: "jack"}
他不会操作YourGlobal.MainPerson本身;同时会保持YourGlobal.MainPerson.People原封不动。如下
YourGlobal.namespace("MainPerson.People");
基于你的单全局对象使用`namespace()`方法可以让开发者放心的认为命名空间总是存在的。这样每个文件都可以首先调用`namespace()`来声明开发者将要使用的命名空间,这样做不会对已经有的命名空间造成任何破坏,这个方法可以让开发者解放出来,在使用命名空间之前不必再去判断它是否存在。
模块
要使用AMD模块,必须需要一个模块加载器,可以使用Require.js
模块加载器来加载AMD模块。其中jQuery,Yui也有其合适的方法。 具体的详细的方法请自行查找文档。
异步模块定义(AMD)
AMD模块,可以你指定的模块名称、依赖、和一个工厂方法,依赖加载完成后执行这个工厂方法。这些内容全部作为参数传入一个全局函数define()
中,其中第一个参数是模块名称;然后是依赖列表,最后是工厂方法。他的每一个依赖都会对应到独立的参数传入工厂方法里,比如:
define("module-name", ["dependency1", "dependency2"],
function (dependency1, dependency2) {
//模块正文
})
可以返回他的公共接口
define("people", ["dependency1", "dependency2"], function (dependency1, dependency2) {
//模块正文
var MainPerson = {};
MainPerson.People = function (title) {
this.title = title;
this.logo = "I am the SuperMan"
}
return MainPerson;
})
零全局变量
情形:一段不会被其他脚本访问到的完全独立的脚本。之所以存在这种情形,是因为所有所需的脚本都会合并到一个文件,或者因为这段非常短小且不提供任何接口的代码会被插入至一个页面中。如下:
(function (win) {
var doc=win.document;
//定义其他的变量
//执行其他的相关的代码
console.log(doc.querySelectorAll("div"));
})(window)
这段立即执行的代码传入了window对象,因此这段代码不需要直接引用任何全局变量。在这个函数内部,变量doc是指向document对象的引用,只要是函数的代码中没有直接修改window或doc且所有变量都是var关键字来定义,这段脚本则可以注入到页面中而不会产生任何全局变量。
这种模式的使用场景有限:只要你的代码需要被其他的代码所依赖,就不能使用这种零全局变量的方式,如果你的代码需要在运行时被不断扩展或修改也不能使用零全局变量的方式,但是,如果你的脚本非常短,且不需要和其他代码产生交互,可以考虑使用零全局变量的方式实现代码。
事件处理
如下面的代码:
//不好的写法
function move(e) {
console.log(e);
}
addListener(document.querySelector("#abc"),"click",move);
function addListener(target,type,handler) {
if(target.addEventListener){
target.addEventListener(type,handler);
}else if(target.attachEvent){
target.attachEvent("on"+type,handler);
}else {
target["on"+type]=handler;
}
}
虽然这段代码看起来非常简单且没有什么问题,但实际上是不好的写法,因为这种做法有其局限性。
典型用法
规则一: 隔离应用逻辑
将应用逻辑从所有事件处理程序中抽离出来的做法是一种最佳的实践,因为不知道什么时候会用到同一逻辑;
影响测试:测试时需要直接触发功能代码,而不必通过模拟对元素的点击来触发。如果这样将应用逻辑放置于事件处理程序中,唯一的测试方法是制造事件的触发。尽管某些测试框架可以模拟触发事件,但实际上这不是测试的最佳方法。调用功能性代码最好的做法就是单个函数调用。
//好的写法
var MyApp={
move:function (e) {
console.log(e.target.innerText);
var popup=document.querySelector("#popup");
popup.style.left=e.clientX+"px";
popup.style.top=e.clientY+"px";
popup.className="popup";
},
handleClick:function (e) {
this.move(e);
},
}
addListener(document.querySelector("#abc"),"click",function (e) {
MyApp.handleClick(e)
});
这里的事件处理程序的逻辑都转移到
MyApp.move
中;
规则二: 不要分发事件对象
上面的还有一个问题,即event对象被无节制的分发。他从匿名事件处理函数传入了MyApp.handleClick()
,然后又传入了MyApp.move
;而实际中,event对象上包含很多和事件相关的信息,而实际中只用几个;
最佳的办法是让事件处理程序使用event对象来处理事件,然后拿到所有需要的数据传给应用逻辑。如上面的只需要x和y两个坐标。
var MyApp={
move:function (x,y) {
var popup=document.querySelector("#popup");
popup.style.left=x+"px";
popup.style.top=y+"px";
popup.className="popup";
},
handleClick:function (e) {
this.move(e.clientX,e.clientY);
},
}
addListener(document.querySelector("#abc"),"click",function (e) {
MyApp.handleClick(e)
});
重写之后,仅仅将x,y的值传入MyApp.move
;可以很清晰的对这段代码进行测试。
//可以这样测试
MyApp.move(10,10)
当处理事件时,最好让事件处理程序成为接触到event对象的唯一函数。事件处理程序应当在进入应用逻辑之前针对event对象执行任何必要的操作,包括默认事件或阻止事件冒泡,都应当直接包含在事件处理程序中。
var MyApp={
move:function (x,y) {
var popup=document.querySelector("#popup");
popup.style.left=x+"px";
popup.style.top=y+"px";
popup.className="popup";
},
handleClick:function (e) {
e.preventDefault();
e.stopPropagation();
this.move(e.clientX,e.clientY);
},
}
addListener(document.querySelector("#abc"),"click",function (e) {
MyApp.handleClick(e)
});
避免“空比较”
检测原始值:typeof
可以对比出5中原始类型:字符串(“string”),数字(“number”),布尔值(“boolean”),null和undefined ;
typeof variable= typeof(variable)
检测引用值:instanceof
使用typeof 检测null ;返回“object”;这是很严重的bug;所以又引入了instanceof;语法:
value instanceof constructor;
var now=new Date();
console.log((now instanceof Object)); //true
console.log((now instanceof Date)); //true
对于函数和数组;这两个类型来说,一般用不到instanceof
检测函数
检测函数的最好使用typeof,直接返回function
;
检测数组
一个新的检测方式
Object.prototype.toString.call(value) //"[object ***]"
console.log(Object.prototype.toString.call(now));//[object Date]
检测属性
当检测一个属性是否在对象存在时:
此时不要与空值(null,undefined)进行比较;这样的话,就是通过给定的名字来检查属性的值。如:
//不好写法
if(obj[propertyname]){
//执行代码
}
if(obj[propertyname]!=null){
//执行代码
}
if(obj[propertyname]!=undefined){
//执行代码
}
而非判断给定的名字所指的属性是否存在,当其属性的值为0,“ ”,false,null和undefined时,上面的做法就不合适了。
所以最好的方法使用in
运算。其仅仅简单的判断属性是否存在,而不会去读属性值。如果实例对象的属性存在、或者继承自对象的原型,in
运算符都会返回true。如:
var obj={
num:0,
content:null,
}
if("content" in obj){
//执行
console.log(obj["content"]);//null
}
if(obj["content"]){
//不执行
console.log(obj["content"]);
}
if("num" in obj){
//执行
console.log(obj["num"]);//0
}
if(obj["num"]){
//不执行
console.log(obj["num"]);
}
还有一个检查实例对象某个属性存在的方法是hasOwnProperty()
。所有继承自Object和JavaScript对象都有这个方法,如果实例中存在这个属性则返回true,(如果这个属性只存在原型里,则返回false)。
if(obj.hasOwnProperty("num")){
console.log((obj.hasOwnProperty("num")));//true
console.log(obj["num"]);//0
}
区别:in操作符只要通过对象能访问到属性就返回true。hasOwnProperty()只在属性存在于实例中时才返回true。用“In”来查找是深度查找 查找原型链里 是否有这……属性。 而平时小项目常用“hasOwnProperty”来查找(查找自身小范围内是否有此属性,可能更快捷高效。而“in”有特殊需求时使用。);in判断的是对象的所有属性,包括对象实例及其原型的属性;
而hasOwnProperty则是判断对象实例的是否具有某个属性。
将配置数据从代码中分离出来
配置数据
配置数据就是应用中写死的值。例如
- URL
- 需要展现给用户的字符串
- 重复的值
- 设置(比如每页的配置项)
- 任何可能发生变更的值。
抽离配置数据
将配置数据拿到外部,从JavaScript代码中拿掉。单独放入一个文件中。
保存配置数据
配置数据最好放在单独的文件中,以便清晰的分隔数据和应用逻辑。格式的话,就是现在像大家目前所知的JSON格式。或者放入js文件中,建立一个名为config
的对象。例子:
var config={"URL_INVAILD":"/errors/invaild.php","INVAILD_VALUE":"INVAILD value"}
抛出自定义错误
在JavaScript中抛出错误
throw new Error("something bad");
如果你这样写:throw "something bad";
这样确实能抛出一个错误;但是火狐、欧朋、谷歌则会显示Uncaught something bad
的错误;而且,可以抛出任何类型的数据。
注意:如果没有通过try-catch语句捕获,抛出任何值都将引发一个错误。因此为了针对所有的浏览器,唯一不出错的显示自定义的错误消息的方式就是用一个Error对象。
抛出错误的好处 ##
function getDivs(e) {
if(e&&e.getElementsByTagName){
return e.getElementsByTagName("div");
}else {
throw new Error("getDivs():参数必须是一个DOM元素");
}
}
抛出自己的错误可以帮助自己快速帮助自己调试问题。最好在错误信息中包含函数名称+函数失败的原因。
何时抛出错误
不要抛出每一个错误,那样会对脚本的整体性能造成影响;只需抛出关键错误。
- 一旦修复了一个很难调试的错误,尝试增加一两个自定义错误。当再次发生错误时,这将有助于更容易的解决问题。
- 如果正在编写代码,思考一下:“我希望【某些事情】不会发生,如果发生,我的代码会一团糟糕”。这是,如果“某些事情”发生,就抛出一个错误。
- 如果正在编写的代码别人也会使用,思考一下他们使用的方式,在特定的情况下抛出错误。
我们只是为了,更加方便的调试。
try-catch
可能引发错误的代码放在try块中,处理错误的代码放在catch块中。
try {
//引发错误的代码
}
catch (e){
//处理错误的代码
}
当在try
中发生一个错误时,程序立即停止执行,然后跳到catch块,并传入一个错误对象。检查该对象可以确定从错误中恢复的最佳动作。
当然,还可以加一个finally。在finally
中的代码,不管是否有错误发生,最后都会执行。例如:
try {
//引发错误的代码
}
catch (e){
//处理错误的代码
}finally {
//都会执行的代码
}
但是,如果try中包含一个return语句,实际上它必须等到finally块中的代码执行后才能返回。
错误类型
- Error:所有的错误基本类型。实际上引擎从来不会抛出该类型的错误。
- EvalError:通过eval()函数执行代码发生错误时抛出。
- RangeError:一个数字超出它的边界抛出。
- ReferenceError:期望的对象不存在时抛出—-试图在一个null对象引用上调用一个函数。
- SyntaxError:给eval()函数传递的代码中有语法错误时抛出。
- TypeError:变量不是期望的类型抛出。—-new 10或 “prop”in true;
- URIError:给encodeURI()、encodeURIComponent()、decodeURI()或者decodeURIComponent()等函数传递格式非法的URI字符串时抛出。
可以根据不同的错误类型,来抛出不同的错误消息。
不是你的对象不要动
注意:如果你的代码没有创建这些对象,不要修改他们。
- 原生对象(Object、Array等等)
- dom对象(document)。
- 浏览器对象(BOM)对象(如:window)
- 类库的对象。
原则
1.不覆盖方法。
覆盖一个非自己拥有的对象的方法。如修改document的方法,会导致很多问题。
2.不新增方法。
如:
Array.prototype.reverseSort=function () {
return this.sort().reverse();
}
在原生对象上新增了方法,可能造成命名冲突。可能会导致难以维护的bug。如果想新增方法可以编写自己的插件。
3.不删除方法
最简单的删除一个方法就是给对应的名字赋值为null;
document.getElementsByClassName=null;
将一个方法设置为null,不管它以前是怎么定义的,现在它已经不能被调用了。
当然如果是对象上的实例,也可以使用delete来删除。
继承
1.基于对象的继承
可以使用ES5的Object.create()
;来实现继承。
var obj = {
num: 0,
content: null,
name:"tomny",
sayHi: function () {
console.log((this.name));
}
}
var newobj=Object.create(obj);
newobj.sayHi();
同时,也可以在不需要同名变量在新的对象上在重新定义一遍。
newobj.sayHi=function () {
console.log("jack");
}
newobj.sayHi();//jack
obj.sayHi();//tomny
Object.create()
可以指定第二个参数,该参数对象中的属性和方法都将添加到新的对象中。
var myobj=Object.create(obj,{
name:{
value:"hannah"
}
});
myobj.sayHi();//hannah
obj.sayHi();//tomny
2.基于类型的继承
function Person(name) {
this.name=name;
}
function Author(name) {
Person.call(this,name);//继承构造器
}
Author.prototype=new Person();
阻止修改
在es5中,有三种锁定修改的级别:
防止扩展: 禁止为对象“添加”属性和方法,但已经存在的属性和方法是可以被修改或删除。
密封: 类似“防止扩展”,而且禁止为对象“删除”已经存在的属性和方法。
冻结:类似“密封”,而且禁止为对象“修改”已存在的属性和方法。
每种锁定的类型都拥有两个方法:一个用来实施操作,另一个用来检测是否应用了相应的操作。如防止扩展,Object.preventExtensions()
和Object.isExtensible()
。
var Person={
name:"Jenny",
};
Object.preventExtensions(Person);//锁定对象
console.log(Object.isExtensible(Person));//false
Person.age=15;
console.log(Person.age);//undefined
上面锁定了Person
对象防止被扩展,所以调用Object.isExtensible()
函数返回false;
使用Object.seal()
函数来密封一个对象。可以使用Object.isSealed()
函数来检测一个对象是否已被密封。
Object.seal(Person);//锁定对象
console.log(Object.isExtensible(Person));//false
console.log(Object.isSealed(Person));//true
delete Person.name;
Person.age=15;
console.log(Person);//{name: "Jenny"}
当一个对象被密封时,它已经存在的属性和方法不能被删除,故在非严格模式下,试图删除上例中的name属性将会悄悄的失败。在严格模式下,试图删除属性或方法将会抛出一个错误。被密封的对象同时也是不可扩展的,所以调用Object.isExtensible()
函数返回false;
使用Object.freeze()
函数来冻结一个对象。可以使用Object.isFrozen()
函数来检查一个对象是否已被冻结。
Object.freeze(Person);//锁定对象
console.log(Object.isExtensible(Person));//false
console.log(Object.isSealed(Person));//true
console.log(Object.isFrozen(Person));//true
delete Person.name;
Person.age=15;
console.log(Person);//{name: "Jenny"}
被冻结的对象同时也是不可扩展和密封的。被冻结的对象和被密封的对象最大的区别在于,前者禁止任何对已存在属性和方法的修改。
浏览器嗅探
1.用户代理检测(user-agent);
console.log(navigator.userAgent);
//Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36
//本文章都是用的是谷歌浏览器
由于浏览器为了确保其兼容性,都会复制另一个浏览器的用户代理字符串。
2.特性检测
if(document.getElementById()){
//do something
}
在万恶的旧时代这些还是很有用的,现在自从edge出来之后,除了某些特定的,其余的我是觉得没有多大用处了。
最后,为了避免一些js方法不能使用;尽量去检测该方法是否可以使用,而不是检测浏览器版本。