斷言主要應用於「調試」與「測試」前端
仔細地查找一下JavaScript中的API,實際上並無多少關於斷言的方法。惟一一個就是console.assert:vue
// console.assert(condition, message)
const a = '1'
console.assert(typeof a === 'number', 'a should be Number')
複製代碼
當condition爲false時,該方法則會將錯誤消息寫入控制檯。若是爲true,則無任何反應。git
實際上,不多使用console.assert方法,若是你閱讀過vue或者vuex等開源項目,會發現他們都定製了斷言方法:github
// Vuex源碼中的工具函數
function assert (condition, msg) {
if (!condition) {
throw new Error(`[Vuex] ${msg}`)
}
}
複製代碼
Node中內置斷言庫(assert),這裏咱們能夠看一個簡單的例子:算法
try {
assert(false, '這個值應該是true')
} catch(e) {
console.log(e instanceof assert.AssertionError) // true
const { actual, expected, operator } = e
console.log(`實際值: ${actual},指望值: ${expected}, 使用的運算符:${operator}`)
// 實際值: false,指望值: true, 使用的運算符:==
}
複製代碼
assert模塊提供了很多的方法,例如strictEqual、deepStrictEqual、notDeepStrictEqual等,仔細觀察這幾個方法,咱們又得來回顧一下JavaScript中的相等比較算法:vuex
幾個方法的區別能夠查看這多是你學習ES7遺漏的知識點。api
在Node10.2.0文檔中你會發現像assert.equal、assert.deepEqual這樣的api已經被廢除,也正是避免==的複雜性帶來的易錯性。而保留下來的api基本上可能是採用後幾種算法,例如:app
從上面的例子能夠發現,JavaScript中內置的斷言方法並非特別的全面,因此這裏咱們能夠選擇一些三方庫來知足咱們的需求。函數
這裏咱們能夠選擇chai.js,它支持兩種風格的斷言(TDD和BDD):工具
const chai = require('chai')
const assert = chai.assert
const should = chai.should()
const expect = chai.expect
const foo = 'foo'
// TDD風格 assert
assert.typeOf(foo, 'string')
// BDD風格 should
foo.should.be.a('string')
// BDD風格 expect
expect(foo).to.be.a('string')
複製代碼
大部分人多會選擇expect斷言庫,的確用起來感受不錯。具體能夠查看官方文檔,畢竟確認過眼神,才能選擇適合的庫。
expect.js不只提供了豐富的調用方法,更重要的就是它提供了相似天然語言的鏈式調用。
談到鏈式調用,咱們通常會採用在須要鏈式調用的函數中返回this的方法實現:
class Person {
constructor (name, age) {
this.name = name
this.age = age
}
updateName (val) {
this.name = val
return this
}
updateAge (val) {
this.age = val
return this
}
sayHi () {
console.log(`my name is ${this.name}, ${this.age} years old`)
}
}
const p = new Person({ name: 'xiaoyun', age: 10 })
p.updateAge(12).updateName('xiao ming').sayHi()
複製代碼
然而在expect.js中並不只僅採用這樣的方式實現鏈式調用,首先咱們要知道expect其實是Assertion的實例:
function expect (obj) {
return new Assertion(obj)
}
複製代碼
接下來看核心的Assertion構造函數:
function Assertion (obj, flag, parent) {
this.obj = obj;
this.flags = {};
// 經過flags記錄鏈式調用用到的那些標記符,
// 主要用於一些限定條件的判斷,好比not,最終返回結果時會經過查詢flags中的not是否爲true,來決定最終返回結果
if (undefined != parent) {
this.flags[flag] = true;
for (var i in parent.flags) {
if (parent.flags.hasOwnProperty(i)) {
this.flags[i] = true;
}
}
}
// 遞歸註冊Assertion實例,因此expect是一個嵌套對象
var $flags = flag ? flags[flag] : keys(flags)
, self = this;
if ($flags) {
for (var i = 0, l = $flags.length; i < l; i++) {
// 避免進入死循環
if (this.flags[$flags[i]]) {
continue
}
var name = $flags[i]
, assertion = new Assertion(this.obj, name, this)
// 這裏要明白修飾符中有一部分也是Assertion原型上的方法,例如 an, be。
if ('function' == typeof Assertion.prototype[name]) {
// 克隆原型上的方法
var old = this[name];
this[name] = function () {
return old.apply(self, arguments);
};
// 由於當前是個函數對象,你要是在後面鏈式調用了Assertion原型上方法是找不到的。
// 因此要將Assertion原型鏈上的全部的方法設置到當前的對象上
for (var fn in Assertion.prototype) {
if (Assertion.prototype.hasOwnProperty(fn) && fn != name) {
this[name][fn] = bind(assertion[fn], assertion);
}
}
} else {
this[name] = assertion;
}
}
}
}
複製代碼
爲何要這樣設計?個人理解是:首先expect.js的鏈式調用充分的體現了調用的邏輯性,而這種嵌套的結構真正的體現了各個修飾符之間的邏輯性。
因此咱們能夠這樣書寫:
const student = {
name: 'xiaoming',
age: 20
}
expect(student).to.be.a('object')
複製代碼
固然這並無完,對於每個Assertion原型上的方法多會直接或者間接的調用assert方法:
Assertion.prototype.assert = function (truth, msg, error, expected) {
// 這就是flags屬性的做用之一
var msg = this.flags.not ? error : msg
, ok = this.flags.not ? !truth : truth
, err;
if (!ok) {
// 拋出錯誤
err = new Error(msg.call(this));
if (arguments.length > 3) {
err.actual = this.obj;
err.expected = expected;
err.showDiff = true;
}
throw err;
}
// 爲何這裏要再建立一個Assertion實例?也正是因爲expect實例是一個嵌套對象。
this.and = new Assertion(this.obj);
};
複製代碼
而且每個Assertion原型上的方法最終經過返回this來實現鏈式調用。因此咱們還能夠這樣寫:
expect(student).to.be.a('object').and.to.have.property('name')
複製代碼
到此你應該已經理解了expect.js的鏈式調用的原理,總結起來就是兩點:
因此咱們徹底能夠這樣寫:
// 強烈不推薦 否則怎麼能屬於BDD風格呢?
expect(student).a('object').property('name')
複製代碼
喜歡本文的小夥伴們,歡迎關注個人訂閱號超愛敲代碼,查看更多內容.