classNames
是一个简单的且实用的JavaScript
应用程序,可以有条件的将多个类名组合在一起。它是一个非常有用的工具,可以用来动态的添加或者删除类名。
仓库地址:classNames
使用
根据classNames
的README
,可以发现库的作者对这个库非常认真,文档和测试用例都非常齐全,同时还有有不同环境的支持。
其他的就不多介绍了,因为库的作者写的很详细,就直接上使用示例:
var classNames = require('classnames');
classNames('foo', 'bar'); // => 'foo bar'
- 可以是多个字符串
classNames('foo', 'bar'); // => 'foo bar'
- 可以是字符串和对象的组合
classNames('foo', { bar: true }); // => 'foo bar'
- 可以是纯对象
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); // => ''
- 可以是多个对象
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'
- 多种不同数据类型的组合
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'
- 假值会被忽略
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'
- 可以是数组,数组中的元素可以是字符串、对象、数组,会被展平处理
var arr = ['b', { c: true, d: false }];
classNames('a', arr); // => 'a b c'
- 可以是动态属性名
let buttonType = 'primary';
classNames({ [`btn-${buttonType}`]: true });
还有其他的使用方式,包括在React
中的使用,可以去看看README
,接下里就开始阅读源码。
源码阅读
先来直接来看看classNames
的源码,主要是index.js
文件,代码量并不多:
/*<img src="http://jedwatson.github.io/classname" style="margin: auto" />
*/
/* global define */
(function () { 'use strict'; var hasOwn = {}.hasOwnProperty; function classNames() {var classes = [];for (var i = 0; i < arguments.length; i++) { var arg = arguments[i]; if (!arg) continue; var argType = typeof arg; if (argType === 'string' || argType === 'number') {classes.push(arg); } else if (Array.isArray(arg)) {if (arg.length) { var inner = classNames.apply(null, arg); if (inner) {classes.push(inner); }} } else if (argType === 'object') {if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes('[native code]')) { classes.push(arg.toString()); continue;}for (var key in arg) { if (hasOwn.call(arg, key) && arg[key]) {classes.push(key); }} }}return classes.join(' '); } if (typeof module !== 'undefined' && module.exports) {classNames.default = classNames;module.exports = classNames; } else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {// register as 'classnames', consistent with npm package namedefine('classnames', [], function () { return classNames;}); } else {window.classNames = classNames; }
}());
可以看到,classNames
的实现非常简单,一共就是50
行左右的代码,其中有一些是注释,有一些是兼容性的代码,主要的代码逻辑就是classNames
函数,这个函数就是我们最终使用的函数,接下来就来看看这个函数的实现。
兼容性
直接看最后的一段if
判断,这些就是兼容性的代码:
if (typeof module !== 'undefined' && module.exports) {classNames.default = classNames;module.exports = classNames;
} else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {// register as 'classnames', consistent with npm package namedefine('classnames', [], function () {return classNames;});
} else {window.classNames = classNames;
}
可以看到这里兼容了CommonJS
、AMD
、window
三种方式,这样就可以在不同的环境下使用了。
一下就看到了三种兼容性方式的区别和特性了:
CommonJS
CommonJS
是Node.js
的模块规范,Node.js
中使用require
来引入模块,使用module.exports
来导出模块;
所以这里通过判断module
是否存在来判断是否是CommonJS
环境,如果是的话,就通过module.exports
来导出模块。
AMD
AMD
是RequireJS
在推广过程中对模块定义的规范化产出,AMD
也是一种模块规范,AMD
中使用define
来定义模块,使用require
来引入模块;
所以这里通过判断define
是否存在来判断是否是AMD
环境,如果是的话,就通过define
来定义模块。
window 浏览器环境
window
是浏览器中的全局对象,这里并没有判断,直接使用else
兜底,因为这个库最终只会在浏览器中使用,所以这里直接使用window
来定义模块。
实现
多个参数处理
接下来就来看看classNames
函数的实现了,先来看看他是怎么处理多个参数的:
function classNames() {for (var i = 0; i < arguments.length; i++) {var arg = arguments[i];if (!arg) continue;}
}
这里是直接使用arguments
来获取参数,然后遍历参数,如果参数不存在,就直接continue
;
参考:arguments
参数类型处理
接下来就来看看参数类型的处理:
// ------省略其他代码------
var argType = typeof arg;
if (argType === 'string' || argType === 'number') {// string or numberclasses.push(arg);
} else if (Array.isArray(arg)) {// array
} else if (argType === 'object') {// object
}
这里是通过typeof
来判断参数的类型,只有三种分支结果:
1.string
或者number
,直接push
到classes
数组中;
2.array
,这里是递归调用classNames
函数,将数组中的每一项作为参数传入;
3.object
,这里是遍历对象的每一项,如果值为true
,则将key
作为类名push
到classes
数组中;
string
或者number
的处理比较简单,就不多说了,接下来就来看看array
和object
的处理:
数组处理
// ------省略其他代码------
if (arg.length) {var inner = classNames.apply(null, arg);if (inner) {classes.push(inner);}
}
这里的处理是先判断数组的长度,通过隐式转换,如果数组长度为0
,则不会进入if
分支;
然后就直接通过apply
来调用classNames
函数,将数组作为参数传入,这里的null
是因为apply
的第一个参数是this
,这里没有this
,所以传入null
;
然后获取返回值,如果返回值存在,则将返回值push
到classes
数组中;
参考:apply
对象处理
- 判断对象
toString
是否被重写:
// ------省略其他代码------
if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes('[native code]')) {classes.push(arg.toString());continue;
}
这里的处理是先判断arg
的toString
方法是否被重写,如果被重写了,则直接将arg
的toString
方法的返回值push
到classes
数组中;
这一步可以说是很巧妙,第一个判断是判断arg
的toString
方法是否被重写;
第二个判断是判断Object.prototype.toString
方法是否被重写,如果被重写了,则arg
的toString
方法的返回值一定不会包含[native code]
;
- 遍历对象的每一项:
for (var key in arg) {if (hasOwn.call(arg, key) && arg[key]) {classes.push(key);}
}
这里使用for...in
来遍历对象的每一项;
然后通过Object.prototype.hasOwnProperty.call
来判断对象是否有某一项;
最后判断对象的某一项的值是否为真值,并不是直接判断arg[key]
是否为true
,这样可以处理arg[key]
为不为boolean
的情况;
然后将对象的key
作为类名push
到classes
数组中;
最后函数结束,通过join
将classes
数组转换为字符串,返回;
测试用例
在test
目录下可以看到index.js
文件,这里是测试用例,可以通过npm run test
来运行测试用例;
这里测试用例测试了很多边界情况,通过测试用例上面的代码就可以看出来了:
- 只有为真值的键值才会被保留
it('keeps object keys with truthy values', function () {assert.equal(classNames({a: true,b: false,c: 0,d: null,e: undefined,f: 1}), 'a f');
});
- 参数中如果存在假值会被忽略
it('joins arrays of class names and ignore falsy values', function () {assert.equal(classNames('a', 0, null, undefined, true, 1, 'b'), 'a 1 b');
});
这里还传递了一个
true
,因为是boolean
类型,在程序中是直接被忽略的,所以不会被保留;
- 支持多种不同类型的参数
it('supports heterogenous arguments', function () {assert.equal(classNames({a: true}, 'b', 0), 'a b');
});
- 不会保留无意义的参数
it('should be trimmed', function () {assert.equal(classNames('', 'b', {}, ''), 'b');
});
- 空的参数会返回空字符串
it('returns an empty string for an empty configuration', function () {assert.equal(classNames({}), '');
});
- 支持数组类型的参数
it('supports an array of class names', function () {assert.equal(classNames(['a', 'b']), 'a b');
});
- 数组参数会和其他参数一起合并
it('joins array arguments with string arguments', function () {assert.equal(classNames(['a', 'b'], 'c'), 'a b c');assert.equal(classNames('c', ['a', 'b']), 'c a b');
});
- 多个数组参数
it('handles multiple array arguments', function () {assert.equal(classNames(['a', 'b'], ['c', 'd']), 'a b c d');
});
- 数组中包含真值和假值
it('handles arrays that include falsy and true values', function () {assert.equal(classNames(['a', 0, null, undefined, false, true, 'b']), 'a b');
});
- 嵌套数组
it('handles arrays that include arrays', function () {assert.equal(classNames(['a', ['b', 'c']]), 'a b c');
});
- 数组中包含对象
it('handles arrays that include objects', function () {assert.equal(classNames(['a', {b: true, c: false}]), 'a b');
});
- 深层嵌套数组和对象
it('handles deep array recursion', function () {assert.equal(classNames(['a', ['b', ['c', {d: true}]]]), 'a b c d');
});
- 空数组
it('handles arrays that are empty', function () {assert.equal(classNames('a', []), 'a');
});
- 嵌套的空数组
it('handles nested arrays that have empty nested arrays', function () {assert.equal(classNames('a', [[]]), 'a');
});
- 所有类型的数据,包括预期的真值和假值
it('handles all types of truthy and falsy property values as expected', function () {assert.equal(classNames({// falsy:null: null,emptyString: "",noNumber: NaN,zero: 0,negativeZero: -0,false: false,undefined: undefined,// truthy (literally anything else):nonEmptyString: "foobar",whitespace: ' ',function: Object.prototype.toString,emptyObject: {},nonEmptyObject: {a: 1, b: 2},emptyList: [],nonEmptyList: [1, 2, 3],greaterZero: 1}), 'nonEmptyString whitespace function emptyObject nonEmptyObject emptyList nonEmptyList greaterZero');
});
- 重写
toString
方法的对象
it('handles toString() method defined on object', function () {assert.equal(classNames({toString: function () {return 'classFromMethod';}}), 'classFromMethod');
});
- 处理来自继承的
toString
方法
it('handles toString() method defined inherited in object', function () {var Class1 = function () {};var Class2 = function () {};Class1.prototype.toString = function () {return 'classFromMethod';}Class2.prototype = Object.create(Class1.prototype);assert.equal(classNames(new Class2()), 'classFromMethod');
});
- 在虚拟机上运行
it('handles objects in a VM', function () {var context = {classNames, output: undefined};vm.createContext(context);var code = 'output = classNames({ a: true, b: true });';vm.runInContext(code, context);assert.equal(context.output, 'a b');
});
Css-in-JS
Css-in-JS
是一种将Css
和JavaScript
结合在一起的方法,它允许你在JavaScript
中使用Css
,并且可以在运行时动态地生成Css
。
这种方法的优点是可以在JavaScript
中使用Css
的所有功能,包括变量、条件语句、循环等,而且可以在运行时动态地生成Css
,这样就可以根据不同的状态来生成不同的Css
,从而实现更加丰富的交互效果。
Css-in-JS
的缺点是会增加JavaScript
的体积,因为JavaScript
中的Css
是以字符串的形式存在的,所以会增加JavaScript
的体积。
Css-in-JS
的实现方式有很多种,比如styled-components
、glamorous
、glamor
、aphrodite
、radium
等。
而这个库就是一个将className
可以动态生成的库,在库的README
中有在React
中使用的例子,其实完全可以抛开React
,在任何需要的地方使用。
示例
例如我在普通的HTML
中使用className
,例如有一个按钮,我想根据按钮的状态来动态地生成className
,那么可以这样写:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Document</title><style> .btn {width: 100px;height: 30px;background-color: #ccc;}.btn-size-large {width: 200px;height: 60px;}.btn-size-small {width: 50px;height: 15px;}.btn-type-primary {background-color: #f00;}.btn-type-secondary {background-color: #0f0;} </style>
</head>
<body><button class="btn btn-size-large btn-type-primary" onclick="toggleSize(this)">切换大小</button><button class="btn btn-size-large btn-type-primary" onclick="toggleType(this)">切换状态</button><script src="classnames.js"></script><script> function toggleSize(el) {el.className = classNames('btn', {'btn-size-large': el.className.indexOf('btn-size-large') === -1,'btn-size-small': el.className.indexOf('btn-size-large') !== -1});}function toggleType(el) {el.className = classNames('btn', {'btn-type-primary': el.className.indexOf('btn-type-primary') === -1,'btn-type-secondary': el.className.indexOf('btn-type-primary') !== -1});} </script>
</body>
</html>
总结
classnames
是一个非常简单的库,但是它的功能却非常强大,它可以根据不同的条件来动态地生成className
,这样就可以根据不同的状态来动态地生成不同的className
,从而实现更加丰富的交互效果。
除了React
在使用Css-in-JS
,还有很多库都在使用Css-in-JS
的方式来实现,这个库代码量虽然少,但是带来的概念却是非常重要的,所以值得学习。
其实抛开Css-in-JS
的概念,这个库的实现也很值得我们学习,例如对参数的处理,深层嵌套的数据结构的处理,已经测试用例的完善程度等等,都是值得我们学习的。
最后
整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。
有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享
部分文档展示:
文章篇幅有限,后面的内容就不一一展示了
有需要的小伙伴,可以点下方卡片免费领取