classnames
是现代前端应用程序中常用的实用程序,用于有条件地将 CSS 类名连接在一起。 如果您编写过 React 应用程序,您可能已经使用过类似的库。
实现 classnames
函数。
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: true, bar: false, qux: true }); // 'foo qux'
数组将根据上述规则递归地展平。
classNames('a', ['b', { c: true, d: false }]); // 'a b c'
值可以混合。
classNames('foo',{bar: true,duck: false,},'baz',{ quux: true },); // 'foo bar baz quux'
Falsey 值将被忽略。
classNames(null, false, 'bar', undefined, { baz: null }, ''); // 'bar'
此外,返回的字符串不应有任何前导或尾随空格。
classnames
库在 GitHub 上clsx
库在 GitHub 上:一个较新的版本,用作 classnames
的更快、更小的直接替代品。classnames
是现代前端应用程序中常用的实用程序,用于有条件地将 CSS 类名连接在一起。 如果您编写过 React 应用程序,您可能已经使用过类似的库。
实现 classnames
函数。
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: true, bar: false, qux: true }); // 'foo qux'
数组将根据上述规则递归地展平。
classNames('a', ['b', { c: true, d: false }]); // 'a b c'
值可以混合。
classNames('foo',{bar: true,duck: false,},'baz',{ quux: true },); // 'foo bar baz quux'
Falsey 值将被忽略。
classNames(null, false, 'bar', undefined, { baz: null }, ''); // 'bar'
此外,返回的字符串不应有任何前导或尾随空格。
classnames
库在 GitHub 上clsx
库在 GitHub 上:一个较新的版本,用作 classnames
的更快、更小的直接替代品。以下是向面试官提问以展示您周全思考的好问题。根据他们的回答,您可能需要相应地调整实现。
输入中可以有重复的类吗?输出应该包含重复的类吗?
是的,可以有。在这种情况下,输出将包含重复的类。但是,我们不会测试这种情况。
如果一个类被添加然后被关闭了怎么办?例如
classNames('foo', { foo: false })
?
在库的实现中,最终结果将是 'foo'
。但是,我们不会测试这种情况。
此解决方案的棘手部分是函数的递归性质。因此,我们可以将解决方案分成两部分:
我们需要一个数据结构 classes
来收集递归调用可以访问的函数生命周期内的所有类。在我们的解决方案中,我们使用 Array
作为集合,但您也可以使用 Set
。
为了递归地处理每个参数并收集类,我想到了几种方法:
以下是我们如何处理每种数据类型:
classes
集合中。classes
集合中。classNames
函数或内部递归函数。classes
集合中。在这种方法中,classNames
函数调用自身,其返回值是一个字符串,可以由父递归调用组成。
/*** @param {...(any|Object|Array<any|Object|Array>)} args* @return {string}*/export default function classNames(...args) {const classes = [];args.forEach((arg) => {// Ignore falsey values.if (!arg) {return;}const argType = typeof arg;// Handle string and numbers.if (argType === 'string' || argType === 'number') {classes.push(arg);return;}// Handle arrays.if (Array.isArray(arg)) {classes.push(classNames(...arg));return;}// Handle objects.if (argType === 'object') {for (const key in arg) {// Only process non-inherited keys.if (Object.hasOwn(arg, key) && arg[key]) {classes.push(key);}}return;}});return classes.join(' ');}
在这种方法中,定义了一个内部的 classNamesImpl
辅助函数,它在递归调用中访问顶层的 classes
集合。辅助函数不返回任何内容,它的主要目的是处理每个参数并将其添加到 classes
。
export type ClassValue =| ClassArray| ClassDictionary| string| number| null| boolean| undefined;export type ClassDictionary = Record<string, any>;export type ClassArray = Array<ClassValue>;export default function classNames(...args: Array<ClassValue>): string {const classes: Array<string> = [];function classNamesImpl(...args: Array<ClassValue>) {args.forEach((arg) => {// Ignore falsey values.if (!arg) {return;}const argType = typeof arg;// Handle string and numbers.if (argType === 'string' || argType === 'number') {classes.push(String(arg));return;}// Handle arrays.if (Array.isArray(arg)) {for (const cls of arg) {classNamesImpl(cls);}return;}// Handle objects.if (argType === 'object') {const objArg = arg as ClassDictionary;for (const key in objArg) {// Only process non-inherited keys.if (Object.hasOwn(objArg, key) && objArg[key]) {classes.push(key);}}return;}});}classNamesImpl(...args);return classes.join(' ');}
在这种方法中,定义了一个内部的 classNamesImpl
辅助函数,它接受一个 classesArr
参数。classesArr
在递归调用中被修改并传递,并且所有 classNamesImpl
调用都引用相同的 classesArr
实例。辅助函数不返回任何内容,它的主要目的是处理每个参数并将其添加到 classesArr
参数。
export type ClassValue =| ClassArray| ClassDictionary| string| number| null| boolean| undefined;export type ClassDictionary = Record<string, any>;export type ClassArray = Array<ClassValue>;export default function classNames(...args: Array<ClassValue>): string {const classes: Array<string> = [];function classNamesImpl(classesArr: Array<string>,...args: Array<ClassValue>) {args.forEach((arg) => {// Ignore falsey values.if (!arg) {return;}const argType = typeof arg;// Handle string and numbers.if (argType === 'string' || argType === 'number') {classesArr.push(String(arg));return;}// Handle arrays.if (Array.isArray(arg)) {for (const cls of arg) {classNamesImpl(classesArr, cls);}return;}// Handle objects.if (argType === 'object') {const objArg = arg as ClassDictionary;for (const key in objArg) {// Only process non-inherited keys.if (Object.hasOwn(objArg, key) && objArg[key]) {classesArr.push(key);}}return;}});}classNamesImpl(classes, ...args);return classes.join(' ');}
提供的解决方案没有处理类名的去重,这是一个很好的优化。如果没有去重,classNames('foo', 'foo')
将给你 'foo foo'
,这对于浏览器结果来说是不必要的。
在某些情况下,去重也会影响结果,例如在 classNames('foo', { foo: false })
的情况下,{ foo: false }
出现在参数的后面,所以用户可能并不希望 'foo'
出现在最终结果中。
这可以通过使用 Set
从一开始就收集类名,并在必要时添加或删除类名来处理。
类名去重通常不在面试的范围之内,但可能是一个后续问题。你可以在 Classnames II 中练习去重功能。
Array
转换为 Set
,反之亦然(用于唯一的类名后续问题)typeof []
给出 'object'
,因此您需要在处理对象之前处理数组。供你参考,这是 classnames
npm 包 的实现方式:
var hasOwn = {}.hasOwnProperty;export default 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) {for (var key in arg) {if (hasOwn.call(arg, key) && arg[key]) {classes.push(key);}}} else {classes.push(arg.toString());}}}return classes.join(' ');}
classnames
库在 GitHub 上clsx
库在 GitHub 上:一个更新的版本,作为 classnames
的更快更小的直接替代品。console.log()
语句将显示在此处。