JavaScript模式
概要
这篇文章主要对《javascript模式》这本书上的一些点做了一些笔记和解释。内容主要来源于郑安源读书分享会的ppt。
内容大致分为这几个方面:
- 介绍
- 基本技巧
- 字面量与构造函数
- 函数
- 对象构造模式
浅谈几个属性
在介绍这些概念之前,首先要对原型链,闭包,作用域,上下文,this,这几个js里经常谈论的东西有一定的理解。在下文遇到这几个东西我会简单的解释一下
这里有一篇博客,对这几个性质阐述的非常清楚,如果想理解的稍微透彻一点,吃透这篇博客。。。。
深入理解javascript原型和闭包:http://www.cnblogs.com/wangfupeng1988/p/3977924.html
1.介绍
js的几个概念
Object-based
NO Classes
Prototypes
关于第一点,js是一种对象使用的很普遍的语言。
关于第二点,在之前js是没有class的,也就是类,但是在es6中,js又添入了class的概念(但是现在基本用不到,想了解class的话,请了解java中class的概念,这里不细谈)
关于第三点,也是很重要的一点,也就是原型,每一个函数都有一个prototype属性,这个属性是一个对象,其constuctor属性指向原函数,实现代码复用、继承,在这里作简要的解释
首先要理解什么是构造函数,什么是实例对象
什么是构造函数?和new一起使用的函数就是构造函数。构造函数的目的就是为了方便创建一个实例。方便代码的复用
类似于构造函数就是一个泛指的人,当给了这个人具体特征(名字,特点)的时候,他就变成了一个具体的人,每个具体的人彼此间有差异,这也就是一个个实例
看下面这段代码
function Person(name){ this.name=name; //当和new一起使用时,this指向新实例对象,解释在下一张图 this.sayName=function(){ alert(this.name); }; } //person作为构造函数 var person1 = new person('Bob');//构造了第一个person实例对象 alert(person1.sayName); //Bob var person2 = new person('Taniffer');//构造了第二个person实例对象 alert(person2.sayName); //Taniffer
一般情况下,this指向他上一级对象,person1.sayName; sayName里的this指向的是person1(”.”前面的对象)。当时用构造器的时候,会对构造器下图所示的改动,使得this的指向变为一个空对象。
下面是第三点的解释
每一个对象(包括实例对象)都会有一个proto属性,这条属性是链式的,不断指向它构造函数的prototype属性,一直指向链的末尾,也就是null,这也就是原型链,
当在实例对象中中查找属性时,如果没有,则顺着原型链一直向上查找。
看下面这段代码
function foo(){} foo.prototype.z = 3 //函数foo有一个prototype属性(这个属性值是对象),在这个属性里z=3 var obj =new foo();//在这里构造了一个实例对象,而这个实例对象的原型链先指向构造它的函数的prototype,也就是foo.prototype obj.y = 2; obj.x = 1; obj.x; // 1 obj.y; // 2 obj.z; // 3 这里得到3,是因为在实例对象中没有找到3,顺着原型链向上查找,则得到了这个3 typeof obj.toString; // ‘function' 'z' in obj; // true obj.hasOwnProperty('z'); // false,这个函数是用来检查对象本身是否存在这个属性'z',没有则返回false
2.基本技巧
-
减少全局变量
-
只使用一个var声明
-
声明的提升
-
for循环和for-in循环中的一些注意事项
-
避免使用eval
1.减少全局变量的使用
全局变量的危害:在项目中经常会引用到第三方库,有可能产生命名冲突,导致代码不能工作
2.只使用一个var声明
3.变量声明的提升
在js执行过程中,分为初始化阶段和代码执行阶段,在初始化阶段所有的变量声明会得到提升。
如下图,左边和右边的函数是完全等价的。这在后面函数声明的那一块还会再提到。
4.for循环中的一些注意事项
第一点。关于length
在下图中,左图和右图的代码,哪个更好一点呢?
现在可能看不出来,但是看这句话
具有length属性不一定是数组,还可能是HTMLCollection
什么是HTMLcollection?
在使用dom操作时,有时可能会用到这些操作
var myarray = document.getElementsByClassName()
var myarray = document.getElementsByTagName()
var myarray = document.getElementsByName()
而因为get到的元素有多个,这些dom操作会生成一个类似数组的东西,这个东西就是HTMLcollection,
但是由于HTMLcollection是动态的,所以每次调用myarray.length的时候,都会重新调用 document.getElementsByxxxx() 计算myarray的值,再计算myarray.length,这样就增加了很多不必要的开销。
第二点,关于for-in
for-in循环是一种很好用的遍历方法,可以遍历对象中的元素,但是它有一个缺点就是他会顺着原型链往上遍历,将原型链上的属性也遍历出来。回到上面那张图
for-in会遍历出x,y,z三个值,z可能是我们不想要的。这是我们只需要在遍历的时候加上一些小手段
这样就不会遍历出对象原型链上的值了。
5.避免使用eval
eval是什么?
eval可以使传入的字符串直接当做代码段执行,这样就可能产生全局变量的污染,也可能因为恶意代码产生不安全的因素
3 字面量与构造函数
首先,什么是字面量,字面量就是在声明对象或者数组之类的时候,尽量采用直接把内容写出来的方法,也就是下图的第一种声明方法。
对于第三点,这里做一下解释如下图。
4.函数
1.函数的几种形式
2.关于声明的提升,
这里会连着上文的提升一起做个总结
先看这段代码
func1(); // 输出:我是函数声明 func2(); // 报错 console.log(a); // 输出:undefined function func1() { console.log("我是函数声明"); } var func2 = function() { console.log("我是函数表达式"); } var a = 10;
因为JS会对函数声明前置,所以func1在函数声明前面执行依旧得到正确答案,而函数表达式func2则报错,为什么?我们先要弄清楚:
函数的执行分为两个阶段——变量初始化和代码执行。在变量初始化阶段,声明按照如下顺序提升:
1. 函数参数(若未传入,初始化该参数值为undefined)
2. 函数声明(若发生命名冲突,会覆盖)
3. 变量声明(初始化变量值为undefined,若发生命名冲突,则忽略)
看下面一段代码
a=10; function test(a, b) { var c = 10; function d() {} var e = function _e() {} (function x() {}); b = 20; } test(10); // 变量初始化阶段各个属性的值 AO(test) = { a:10, b:undefined, c:undefined, d:<ref to func "d">, e:undefined }; // 代码执行阶段各个属性的值 AO(test) = { a:10, b:20, c:10, d:<ref to func "d">, e:function _e() {} };
3.回调函数
将函数作为参数传入另一个函数,这个参数就叫回调函数
回调函数的一个错误用例
为什么是undefined呢,在这里,foo(obj.getVal);就相当于将foo(function(){return this.val})传入;
相当于
function foo (){ return this.val; } foo();
这里的this在foo里,相当于global.foo();
所以this指向全局,全局里没有val,输出undefined;
要解决这种错误
可以同时将对象传入
关于call这个函数.
a.call(b);简单来说,就是将a这个方法放在b中来用
在上文就是将getVal这个方法放在obj中来用,也就是obj.getVal;所以输出得到正确的值
4.给自己赋值的函数
5.立即函数表达式
6.带记忆的函数
7.使用对象传递参数
8.生成部分应用函数
通过只给函数提供部分参数,从而生成一个函数,这个生成的函数所接收的参数数量等于之前少传入的参数数量
柯里化函数不会一次性取完所有参数,而是在每次调用时只取一个参数,并返回一个函数来取下一个参数
这样的好处是我们只要以部分的参数来调用某函数,就可以得到一个部分应用函数,部分应用函数所接收
的参数数量等于之前少传入的参数数量。部分应用使得构造新函数快捷方便
5.对象构造模式
1.命名空间
先来看一段相对来说不好的js写法
那么该怎么处理呢,这时候就引入了命名空间
当函数属性发生嵌套,生成的便是嵌套的命名空间
自动生成命名空间的函数
var MYAPP = MYAPP || {}; MYAPP.namespace = function(ns_string) { var parts = ns_string.split('.'), parent = MYAPP, i; // 剥离最前面的冗余全局变量 if (parts[0] === "MYAPP") { parts = parts.slice(1); } for (i=0; i<parts.length; i++) { // 如果不存在,就创建一个属性 parent[parts[i]] = parent[parts[i]] || {}; parent = parent[parts[i]]; } return parent; } // 测试 var module1 = MYAPP.namespace('MYAPP.modules.module1'); module1 === MYAPP.modules.module1; // 忽略最前面的MYAPP var module2 = MYAPP.namespace('modules.module2'); module2 === MYAPP.modules.module2; // 长命名空间 MYAPP.namespace('one.two.three.four.five.six.seven.eight');
2.声明依赖
3.沙盒模式
命名空间也是会产生一些弊端的:
1. 不可避免要产生一个全局变量
2. 如果命名空间嵌套得很深,写起来麻烦
由此便引入了沙盒
将我们的代码写在一个”沙盒“里,这个沙盒产生一个作用域,我们的代码在其中运行,不会产生全局变量
沙盒是封闭的?如果想要使用第三方库怎么办?沙盒还应给我们提供所需的依赖
沙盒的基本框架:
实现沙盒构造函数