良好习惯

back

5 个提升 js 编码水平的实例

:top::top:良好习惯

判断数据类型

typeof 是否能正确判断类型

因为由于历史原因,在判断原始类型时,typeof null会等于object。而且对于对象来说,除了函数,都会转换成 object。

typeof 1; // 'number'
typeof "1"; // 'string'
typeof null; //"object"

typeof []; // 'object'
typeof {}; // 'object'
typeof window.alert; // 'function'

instanceof 是否能正确判断类型

虽然 instanceof 是通过原型链来判断的,但是对于对象来说,Array也会被转换成Object,而且也不能区分基本类型string和boolean

function Func() {}
const func = new Func();
console.log(func instanceof Func); // true

const obj = {};
const arr = [];
obj instanceof Object; // true
arr instanceof Object; // true
arr instanceof Array; // true

const str = "abc";
const str2 = new String("abc");
str instanceof String; // false
str2 instanceof String; // true

Object.prototype.toString.call();

因为每个对象都有一个 toString()方法,当要将对象表示为文本值或以预期字符串的方式引用对象时,会自动调用该方法。默认情况下,从 Object 派生的每个对象都会继承 toString()方法。如果此方法未在自定义对象中被覆盖,则toString()返回[Object type],其中 type 是对象类型。所以就有以下例子:

Object.prototype.toString.call(new Date()); // [object Date]
Object.prototype.toString.call("1"); // [object String]
Object.prototype.toString.call(1); // [object Numer]
Object.prototype.toString.call(undefined); // [object Undefined]
Object.prototype.toString.call(null); // [object Null]
var type = function(data) {
  var toString = Object.prototype.toString;
  var dataType =
    data instanceof Element
      ? "element" // 为了统一DOM节点类型输出
      : toString
          .call(data)
          .replace(/\[object\s(.+)\]/, "$1")
          .toLowerCase();
  return dataType;
};

type("a"); // string
type(1); // number
type(window); // window
type(document.querySelector("h1")); // element

通用的数组/类数组对象封装

但是如果我们要循环一个类数组对象呢?
例如 NodeList。直接循环是会报错的:

document.querySelectorAll("div").map(e => e) // Uncaught TypeError: document.querySelectorAll(...).map is not a function

[...document.querySelectorAll("div")].map(e => e)


var listMap = function(array, type, fn) {
    return !fn ? array : Array.prototype[type]["call"](array, fn)
};
var divs = document.querySelectorAll("div");
listMap(divs, "forEach", function(e) {
    console.log(e)
});

获取 dom 元素节点的偏移量

牛逼的 reduce

数据去重

const data = [
  {
    name: "Kris",
    age: "24"
  },
  {
    name: "Andy",
    age: "25"
  },
  {
    name: "Kitty",
    age: "25"
  },
  {
    name: "Andy",
    age: "25"
  },
  {
    name: "Kitty",
    age: "25"
  },
  {
    name: "Andy",
    age: "25"
  },
  {
    name: "Kitty",
    age: "25"
  }
];

现在我们要去重里面 name 重复的对象,这时候我们可以利用 reduce

const dataReducer = (prev, cur, idx) => {
  let obj = {};
  const { name } = cur;
  obj[name] = cur; //obj:Kris: {name: "Kris", age: "24"}
  return {
    ...prev,
    ...obj
  };
};
const reducedData = data.reduce(dataReducer, {});
let newData = Object.values(reducedData);
// 0: {name: "Kris", age: "24"}
// 1: {name: "Andy", age: "25"}
// 2: {name: "Kitty", age: "25"}

批量生成对象元素

const createList = (item, idx) => {
  let obj = {};
  obj[`a{idx}`] = "data";
  return obj;
};
const listReducer = (acc, cur) => (!acc ? { ...cur } : { ...cur, ...acc });
const obj = Array.from(new Array(20), createList).reduce(listReducer);

强类型检查

back

===代替 ==

变量

back

知名其意的方式为变量命名,通过这种方式,当再次看到变量名时,就能大概理解其中的用意
不要在变量名中添加额外的不需要的单词

let nameValue;
let theProduct;

let name;
let product;

函数

back

函数名称应该是动词或短语,用以说明其背后的意图以及参数的意图。函数的名字应该说明他们做了什么。
避免使用大量参数,理想情况下,函数应该指定两个或更少的参数。参数越少,测试函数就越容易,参数多的情况可以使用对象

function getUsers(fields, fromDate, toDate) {}

function getUsers({ fields, fromDate, toDate }) {}

使用默认参数替代 || 操作
一个函数应该只做一件事,不要在一个函数中执行多个操作
使用Object.assign设置对象默认值(back)

const shapeConfig = {
    type: 'cube',
    width: 200,
    height: null,
};
function createShape = (config){
    config.type = config.type || 'cube';
    config.width = config.width || 200;
    config.height = config.height || 250;
}
createShape(shapeConfig);


const shapeConfig = {
    type: 'cube',
    width: 200,
};
function createShape = (config){
    config = Object.assign({
        type: 'cube',
        widht: 200,
        height: 250,
    }, config);
}
createShape(shapeConfig);

不要使用标志作为参数,因为它们告诉函数做的比它应该做的多(back)

function createFile(name, isPublic) {
  if (isPublic) {
    fs.create(`./public/${name}`);
  } else {
    fs.create(name);
  }
}

function createFile(name) {
  fs.create(name);
}
function createPublicFile(name) {
  createFile(`./public/${name}`);
}

不要污染全局变量,如果需要扩展现有对象,请使用 ES6 类和继承,而不是在原生对象的原型链上创建函数(back)

Array.prototype.myFunc = function myFunc() {};

class SuperArray extends Array {
  myFunc() {}
}

条件

back

避免使用反面条件
使用条件简写,仅对布尔值使用此方法,并且如果确信该值不会是undefined 或null的,则使用此方法

if (isValid === true) {
}

if (isValid) {
}

尽可能避免条件句,而是使用多态性继承

Class Car{
    getMax(){
        switch(this.type){
            case 'Ford':
                return this.someFactor+this.anotherFactor;
            case 'Mazda':
                return this.someFactor;
            case 'Mclaren':
                return this.someFactor-this.anotherFactor;
        }
    }
}


Class Car {
}
Class Ford extends Car {
    getMax(){
        return this.someFactor+this.anotherFactor;
    }
}
……

back

使用链接,许多库(如 jQuery 和 Lodash)都使用这种模式。在类中,只需在每个函数的末尾返回this就可以将更多的该类方法链接到它上。
class 是 JS 中新的语法糖,工作方式就像以前的原型但比原型的方式更简单易懂

JavaScript 设计模式

back

设计原则

back

Tips:在学习设计模式的时候,尽量分开去学,即先学习设计,然后再去学模式,这样的话,对于之后的理解更加容易一些,什么设计呢,其实就是指设计原则,而模式这里指的就是就是我们要讲的设计模式。现在共有 5 大设计原则,不管是哪种设计模式,都是遵循设计原则的。下面来分别介绍这 5 大设计原则。

  • 1.单一职责原则
    单一职责原则原则就是每个程序只负责做好一件事情,如果功能过于复杂就拆分开,每个部分保持独立。这个其实也符合我们当下流行框架 Vue 和 React 的组件化开发,把一个复杂的页面拆分成一些零散的组件,并且每个组件保持独立,同时也可在不同的页面当中实现复用。
  • 2.开放封闭原则
    开发封闭原则大白话的意思就是对扩展开放,对修改封闭。放到实际开发中如何去理解呢,我们日常的网站和 APP 开发每周都有发不同的版本来增加需求,那么增加需求的时候,尽量要做到扩展新代码,而非修改已有代码,如果我们修改已有代码无疑增加了风险,因为本来原来的代码是没有问题的,加了新的代码之后必然会增加不可预知的风险,当然有的个别需求必须修改已有代码,这个另说。同时这个原则也是我们软件设计的终极目标。
  • 3.李氏置换原则
    子类能够覆盖父类,父类能出现的地方,子类就可以出现,这个原则其实在 Java 等语言当中是较为常见的,多用于继承,而 JavaScript 作为弱类型语言,继承使用其实是很少的,这里简单提一下。
  • 4.接口独立原则
    接口独立原则的含义是保持接口的单一独立,避免出现胖接口,JavaScript 中是没有接口(typescript 例外),使用较少, 它是有点类似于单一职责原则,这里更关注接口。
  • 5.依赖倒置原则
    依赖倒置原则的含义是面向接口编程,依赖于抽象而不依赖于具体,使用方只关注接口而不关注具体类的实现,同样这里也是 JavaScript 中使用较少(没有接口&弱类型)

设计模式

back

工厂模式

back

工厂模式概念

工厂模式是由一个方法来确定是要创建哪个类的实例,在前端当中最为常见的工厂模式就是new操作的单独封装,当遇到 new 操作的时候,就要考虑是否该使用工厂模式。这里也可以结合生活中的例子去思考。当你去购买汉堡,直接点餐取餐,不会自己亲手做,商店要“封装”做汉堡的工作,做好直接给买者。也就是说通过提供原材料,最终得到是汉堡还是炸鸡,是由你自己决定的。

工厂模式前端中实例
  • 1.jQuery 当中的$('')
    jQuery 当中的\$('div'),这里的$选择器就是已经封装好的API,这里我们直接使用即可。下面简单实现一个 JQuery 的\$操作符,帮助大家加深理解。
class jQuery {
  constructor(selector) {
    let slice = Array.prototype.slice;
    let dom = slice.call(document.querySelectorAll(selector));
    let len = dom ? dom.length : 0;
    for (let i = 0; i < len; i++) {
      this[i] = dom[i];
    }
    this.length = len;
    this.selector = selector || "";
  }
  append(node) {}
  html(data) {}
  //等等API
}
window.$ = function(selector) {
  return new jQuery(selector);
};
  • 2.Vue 异步组件
    这个大家应该比较熟悉,而且官方文档讲的也非常详细,这里直接饮用官方文档的案例,在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染
Vue.component("async-example", function(resolve, reject) {
  setTimeout(function() {
    // 向 `resolve` 回调传递组件定义
    resolve({
      template: "<div>I am async!</div>"
    });
  }, 1000);
});

单例模式

back

单例模式符合单一职责原则,用大白话描述单例模式就是系统中被唯一使用,例如:电子商务网站常见的购物车和登录都是单例模式的运用。

单例模式前端中的实例
  • 1.jQuery 中的$('')
    仍旧是jQuery当中的$(' ')选择器,整个 jQuery 框架当中有一个这样的选择器。

  • 2.Redux 和 Vuex
    不管是 Redux 还是 Vuex,里面的状态 store 都是唯一的,Redux中的store只能通过Reducer去修改,而 Vuex 中的 store 只能通过 Mutation 修改,其余修改方式都是错误的。

适配器模式

back

适配器模式的含义是旧接口格式和使用者不兼容,中间加一个适配器接口。生活当中随处可见符合适配器模式的例子,如:插头转换器,电脑接口转换器。

适配器模式前端中的实例
ajax({
  url: "/getList",
  type: "Post",
  dataType: "json",
  data: {
    id: "123"
  }
}).done(function() {});

但是这个时候你接到的项目当中都是:\$.ajax({...}),这个时候我们只需要加一层适配器即可,代码如下:

let $ = {
  ajax: function(options) {
    return ajax(options);
  }
};

装饰器模式

back

装饰器模式,装饰我们可以理解为就是给一个东西装饰另外一些东西使其更好看,对应到前端中就是为对象添加新功能,并且不改变其原有的结构和功能,这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。在我们日常生活中用到的手机壳就是经典的例子

代理模式

back

代理模式是使用者无权访问目标对象,中间加代理,通过代理做授权和控制,我们经常会用到这个模式,不管实际的开发也好,还是网络部署当中,都能够看到它的身影。如:科学上网 谷歌搜索

代理模式前端中的实例
  • 1.网页中的事件代理
    其实网页的事件代理也是非常常考的面试题之一,其实就是把元素绑定到父元素上面,而不是对其下面的每一个子元素都进行相应的绑定,下面举一个具体的实例:
<div id="item">
    <a href="#">a1</a>
    <a href="#">a2</a>
    <a href="#">a3</a>
    <a href="#">a4</a>
    <div>
    <button>点击增加一个a标签</button>
<script>
    let div1 = document.getElementById('div1')
    div1.addEventListener('click',function(e) {
        let target = e.target
        if(e.nodeName === 'A') {
            alert(target.innerHTML)
        }
    })
</script>
  • 2.jQuery 中的$proxy
    比如我们经常会遇到这样一种情况,如下代码所示
$("#div1").click(function() {
  $(this).addClass("red");
});
$("#div1").click(function() {
  setTimeout(function() {
    // this 不符合期望
    $(this).addClass("red");
  }, 1000);
});

解决的方法可能有的同学已经想到是先将 this 赋值给一个变量。

$("#div1").click(function() {
  setTimeout(function() {
    let _this = this;
    // this 不符合期望
    $(_this).addClass("red");
  }, 1000);
});

是的这种方法是对的,但是这样就会增加一个变量,所以这里用$proxy解决更好,代码如下:

$("#div1").click(function() {
  setTimeout(
    $proxy(function() {
      $(this).addClass("red");
    }),
    1000
  );
});

观察者模式

back

观察者模式就是只要你作为订阅者订阅了某个事件,当事件触发的时候,发布者就会通知你。这里可以类比我们去咖啡厅点咖啡,当我们点了咖啡之后,就可以去做别的事情,当咖啡完成时,服务员就会叫你来取,你到时候取走咖啡即可。

观察者模式前端中的实例
  • 1.网页中的事件绑定
<button id="btn1">btn</button>
<scirpt>
    $('#btn1').click(function() {
        console.log(1)
    })
</script>
  • 2.Node.js 的自定义事件 EventEmitter
const EventEmitter = require("events").EventEmitter;
const emitter1 = new EventEmitter();
emitter1.on("some", () => {
  //监听some事件
  console.log("some event is occured 1");
});
emitter1.on("some", () => {
  //监听some事件
  console.log("some event is occured 2");
});
emitter.emit("some");

状态模式

back

  • 状态总数(state)是有限的。
  • 任一时刻,只处在一种状态之中。
  • 某种条件下,会从一种状态转变(transition)到另一种状态。