精读JavaScript模式(一)

一、前言

为什么读这本书?

其实做前端开发,一个需求给不同工作经验的人去做,只要完工时间不算苛刻,大家都是能实现的。功能实现虽然大致相同,但当我们回归代码去看实现方式,代码书写的美观程度,以及实现的方法其实是不尽相同的。毕竟经验丰富的人,拿到一个需求,可能脑海里就浮现了多个可供选择的方案,而经验较浅的人,就更偏向于如何实现基本需求了。
例如说到过滤一个数组,第一想到使用for循环,不会想到filter方法;再如做条件判断,首先想到if else,忽略掉了还有which case或Boolean?true:false三元运算符之类的其它选择。说这些不是说后者毕竟比前者要好,毕竟对于不同的使用场景,合适的才是最佳的,但能举一反三,从三种甚至多种方法中做出选择,是肯定要比一招鲜要更好的。
经验的积累不是一天两天的事情,这点我也明白,那能不能先从基本做起,比如了解更好的代码书写规范,掌握好基本概念,知道一些实用的js模式甚至说套路,那这就是我读这本书的原因了。
从这本书,你会知道比常规for循环更优的写法,知道new一个构造函数时究竟发生了什么,知道为什么setTimeout('fun()',1000),setTimeout(fun,1000)两种写法,为什么前者加引号都能执行,知道更优秀的编码方式以及更多有趣的东西。
这个系列只是作为读书笔记,挑出一些重要或者我觉得有趣的的概念,如果觉得有趣,推荐阅读原书。

二、JavaScript概念 

1.面向对象
JavaScript(以下简称js)是一门面向对象的编程语言,我们总说,万物皆对象,这点是没错的。但需要注意的是,js中的六种基本(原始)数据类型不是对象,它们分别是,String(字符串)Number(数字)Boolean(布尔类型)nullundefined,以及ES6新增的基本类型symbol
复杂(引用)数据类型可以归纳为对象类型,而对象有两大类,本地对象与宿主对象。

宿主对象包含window和所有DOM对象,而本地对象包括了内置对象(如 Function,Array,Date)或自定义对象(var o = {});
基本数据类型与引用数据类型的区别在于,基本数据类型的变量名与值都是存放在栈内存中,而对于引用数据类型,变量在栈内存中,值存放中堆内存中,变量名指向由堆内存提供的值地址,不理解可以具体看看博主对于深浅拷贝中值的存放图解。
说到数据类型,null是需要单独说说的。我们可以在浏览器F12调出控制台,输入typeof null回车,可以看到输出为object,这是为什么呢?
在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null的类型标签也成为了 0,typeof null就错误的返回了"object"。

ECMAScript提出了一个修复(通过opt-in),但被拒绝。这将导致typeof null === 'object'。
这段话可以理解为,这是早期JS设计留下的缺陷,但我们只要记住,虽然typeof得到的是object类型,但null本质就是基本数据类型,那我们要判断null类型该怎么办呢,可以使用如下方法。

Object.prototype.toString.call(null) === [object Null]。

2.原型(prototype)
js中的继承是代码重用的一种方式,继承的方式很多,原型就是其中一种,需要注意的是,原型其实就是一个普通对象。我们创建的每一个函数其实都自带prototype属性,这个属性指向一个空对象,你可以为这空对象添加各种属性方法,而这些新增成员可以被其它对象继承,作为其它对象的自有属性。这个空对象也不是严格意义上的空,它自带一个constructo属性,它指向你新建的函数。
3.严格模式
严格模式是采用具有限制性JavaScript变体的一种方式,从而使代码显示地 脱离“马虎模式/稀松模式/懒散模式“(sloppy)模式。添加模式比较简单,你只需要在你希望执行严格模式的作用域添加"use strict"即可。

"use strict"

顾名思义,严格模式相比传统模式有以下改变:(笔试遇到过一次)

• 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;
• 消除代码运行的一些不安全之处,保证代码运行的安全;
• 提高编译器效率,增加运行速度;
• 为未来新版本的Javascript做好铺垫。

二、高质量javaScript基本要点

1.编写可维护的代码

人人都喜欢开发新功能,一切从零开始,不喜欢维护旧代码,特别是一段段密密麻麻没有注释的代码,这点每个开发者都感同身受。但抛开阅读遗留代码或者同事的代码,就算是我们自己开发的功能,两三个月后回头再读也可能出现阅读困难的问题,这就导致了维护成本较高的问题;阅读代码超过开发功能的时间很明显是不合理的。
因此我们在开发新功能,或者维护旧代码的同时,就得花时间为不合理的代码进行调整,例如
• 可读的
• 一致的(看起来像同一个人写的,有统一的规范)
• 可预测的(能拓展)
• 有文档的(或注释)

2.减少全局对象

减少全局对象基本是每个前端者入行就被告知的点。js是使用函数来管理作用域(scope)的,那么可以说,在一个函数内定义的变量就是一个局部变量,局部变量在当前作用域外部是不可见的;反之,全局变量是不在任何函数体内声明的变量,或者是直接使用而未申明的变量。

function echo() {
    var a = 1;//局部
    b = 2;//虽然在函数体内,但是未使用var let之类申明。
};
var c = 3//全局,虽然有申明,但是在函数体外。

每个js运行环境都有一个隐式全局对象,通常浏览器用全局对象window代表这个全局对象隐式全局对象,我们创建的每个全局变量都是这个全局对象的属性,我们可以不在任何函数体内使用this就能查看这个全局对象的引用,如:

a = 1;
console.log(a);//1
console.log(this.a);//1
console.log(window.a)//1
console.log(window['a'])//1

全局变量在js代码执行的整个作用域都是可见的,正因为它们存在于同一个命名空间中,所以会发生命名冲突的问题。我们很难保证自己定义的全局变量是否与三方库,插件中变量是否有重名,所以使用变量先去申明它是非常重要的。
顺带一提

function echo() {
    var a = b = 0;
}

其中b是全局变量,a是局部变量,等价于var a = (b = 0);(实际开发中肯定是不推荐这样的写法,可读性太差,只是书中有举例,顺带说说这种写法带来全局变量的问题)
隐式全局变量与显式全局变量
隐式全局变量:通过 var 创建的全局变量(在任何函数体之外创建的变量)不能被删除。
隐式全局变量:没有用 var 创建的隐式全局变量(不考虑函数内的情况)可以被删除。

var a = 1;
b = 2;
console.log(delete a)//false
console.log(delete b)//true

隐式全局变量并不算是真正的变量,可以说它们是全局对象的一个属性成员。而属性是可以通过delete运算符删除的,变量不可以被删除,这是两者的区别。

3.访问全局对象

我们在前面说,全局变量总是被隐性的添加为全局对象的属性,那么我们其实可以通过全局对象来访问全局变量,例如通过window。但并不是在所有的环境下默认隐性全局对象都是window,或者说某个环境的全局对象可能不叫window。但我们可以利用根据this指向原则始终能找到全局对象,
例如函数在自调情况下,this总是指向全局对象(严格模式下this会指向undefined)。

var global = (function (){
    return this;
})();
console.log(global)//当前环境的全局对象

4.单 Var 模式

申明变量在编程中是高频率的,在函数顶部使用一个单独的var语句是非常推荐的一种模式。这么做有如下好处:

• 在同一个位置可以查找到函数所需的所有变量(变量集中,方便查找)
• 避免当在变量声明之前使用这个变量时产生的逻辑错误(申明提前的问题)
• 提醒你不要忘记声明变量,顺便减少潜在的全局变量
• 代码量更少(输入更少且更易做代码优化)

var a = 1,
    b = 2,
    c,
    fun = function () {};

当然使用let const申明也是可以使用这种模式的,而且let申明变量也彻底解决了var申明提前这种较为诟病的问题,这里还是按照书中思路去整理了笔记,大家心里能明白就好。

5.申明提前:分散的var带来的问题

首先,申明提前可以说是var申明模式的一个隐性问题,有时候会带来一些不必要的麻烦,而在ES6中新增的let申明方式其实已经解决了var的申明提前问题,本来这一点可说可不说,但毕竟还是有一些面试题会说道,就简单带一带。
对于js来说,当我们在某个作用域(比如同一个函数内)里声明了一个变量,这个变量在整个作用域内都是可见的,可使用的,包括在 var 声明 语句之前,这种情况就是所谓的申明提前。

(function (){
    console.log(a);//undefined
    var a = echo;
    console.log(a);//echo
})();

在这段代码中,尽管第一个console在变量a申明之前,它也不会报错,因为在这个函数体内,var a申明会提前(赋值不提前),任何一个地方,不管先后都能正确的使用它,它等同于

(function (){
    var a;
    console.log(a);//undefined
    a = echo;
    console.log(a);//echo
})();

6.更优的for 循环

这里不讨论for forEach while各类循环方法的性能优劣,毕竟循环之争一直存在,可读性,性能太多因素,还是根据实际使用场景来定夺,后面有空也确实想对于现有常用数据遍历可行方法进行一个整理。(应该不会鸽)
我们最常见的for循环写法

for (var i = 0; i < arr.length; i++) {
  //do something with arr[i];
}

在for 循环括号中,var i = 0其实只会申明一次,但i < arr.length 与i++是每次循环都会执行的。那么就存在一个问题,上面的代码每次循环都会重复取一次数组arr的length属性,这会降低代码的性能,特别是当arr不单单是个数组,而是一个HTMLCollection对象时。
HTMLCollection对象是由DOM方法返回的对象,例如:
• document.getElementsByName()
• document.getElementsByClassName()
• document.getElementsByTagName()
操作dom是一个很耗资源的行为,如果每次循环都要遍历查询dom元素显然不太合理,更好的做法是用变量一开始就保存数组的长度。

for (var i = 0, max = myarray.length; i < max; i++) {
  // do something with myarray[i]
}

或者这样,将变量的申明统一在一起,for只用管好自己的循环。

var i = 0,
myarray = [],
max = myarray.length;
for (; i < max; i++) {
  // do something with myarray[i]
}

注意括号中的第一个分号我有保留,或者写成(i < max; i++;)也可以,分号不能丢,不然会报错。
通过上面的改写,不管循环多少次,其实都只用查询一次DOM节点的length,是不是比较nice。
对于for循环,其实还可以做少量的改进,在for中我们之所以申明i = 0的作用是告诉循环,i是从0开始递增并与max做判断是否需要继续下次循环,其实我们可以直接获取数组长度让其递减,效果是一样的。

var myarray = [],
i = myarray.length
for (; i --;){
  // do something with myarray[i]
}

这样写分号总觉得有点奇怪,我们也可以使用while来进行代替

var myarray = [],
i = myarray.length;
while (i--) { //在某篇博客看到过while 比 for更快的说法
  // do something with myarray[i]
}

这么做相比前面的写法有两个有点,第一,变量减少了,我们直接让将length赋予给i进行递减,省去了变量max,其次,递减到0的做法速度会更快,因为与零相比要比和非零数字或者数组长度比较要高效跟多。

第一篇就先记到这里,再写下去篇幅就太长了点,看着就不太想想读了,不过估计也没人会耐着性子读这样的文章吧。

第二篇也会抓紧时间写,倘若有人阅读过,也欢迎指出错误。

原文地址:https://www.cnblogs.com/echolun/p/9827487.html