Classnames 简单介绍
Classnames是一个开发过程中经常能用到的类库。它的作用就是将多个className拼接到一块,还可以加入条件判断。
仓库地址:https://github.com/JedWatson/classnames
安装方式1
2
3
4
5
6
7
8# via npm
npm install classnames
# via Bower
bower install classnames
# or Yarn (note that it will automatically save the package to your `dependencies` in `package.json`)
yarn add classnames
我们可以先来简单地看一下它的用法。
最基础的两个className拼接,接收多个className返回拼接好的字符串
1
classNames('foo', 'bar'); // => 'foo bar'
也可以接收对象,对象的key作为要拼接的className,key所对应的value为一个布尔值,来表示是否拼接。true拼接,false不拼接。一般用于控制一个元素的显隐。
1
2classNames('foo', { bar: true }); // => 'foo bar'
classNames('foo'. { bar: false }); // => 'foo'也可以传一个数组,就像这样:
1
2var arr = ['b', { c: true, d: false }];
classNames('a', arr); // => 'a b c'与ES6的字符串模板配合,使用动态class name:
1
2let buttonType = 'primary';
classNames({ [`btn-${buttonType}`]: true });
源码初探
源码很简单,大概30多行。我们可以先来简单的看一下整个源码。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35var 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) {
classes.push(arg.toString());
} else {
for (var key in arg) {
if (hasOwn.call(arg, key) && arg[key]) {
classes.push(key);
}
}
}
}
}
return classes.join(' ');
}
接下来我们来详细分析一下。var hasOwn = {}.hasOwnProperty;
这一行我们先不看。我们直接进入方法。
- 看第一个for循环:
作用就是遍历这个方法传入的参数,然后判断参数,如果为falsy(如:null,undefined,false,’’)就直接返回。接下来就判断参数类型,根据类型分别进不同的判断条件。这里有两个知识点【arguments对象】和【JS的类型判断】。 - 第一个if条件很简单,如果传入参数为string类型或者number类型的话,就直接push到classes中,最后直接join(‘ ‘)即可。
- 第二个if,就是判断传入的参数是数组的时候。这里有一点,把判断为数组放在判断是否为对象之前因为type Array 返回的也是”object”,数组也是对象这很正常。这块主要就是递归调用这个方法。也是存在两个知识点【JS的数组判断】和【JS的递归实现】
- 最后一个if就是当传入的参数为一个对象的时候。
arg.toString !== Object.prototype.toString
就是判断,是否是Object对象。如果不是的话就直接push toString()的结果。
如果是Object对象的话。直接用for…in遍历它。这里有一个判断是否是对象它自身的属性。如果既是自身属性,属性值又为真的时候。就将这个属性名(也就是key)push到classes数组中。至此整个源码就分析完毕了。在这里if里涉及到的知识点有。【Object.prototype.toString() 与Object.toString()的区别】、【for…in陷阱】、【为什么不直接用hasOwnProperty判断】。
知识点讲解
arguments 对象
常用方法
arguments是一个类数组对象,它的prototype并不是指向Array而是指向Object,代表传入function的参数列表。
一般通过数组下标的方式来访问每一个参数,如arguments[0]、arguments[1]]。
通过arguments.length 来获取传入了几个参数。
arguments转数组
在ES6之前,通常用Array.prototype.slice.call(arguments);
或者更简单地方法[].slice.call(arguments)
来将arguments对象转换为数组。
简单地讲一下,一般slice()方法,一般用于返回一个新的数组对象,这一对象是一个由传入的两个参数begin和end决定的原数组的浅拷贝(包括begin,不包括end)。原数组不会改变。
我们知道JS中,Array也不是真正的Array,也是一个类数组。于是只要我们构建一个类数组,将this绑定到该类数组上,不传入begin和end就可以实现该类数组转数组。 这块比较抽象。
ES6出来之后,我们就可以愉快地用Array.from()
方法,来对类数组对象转为数组对象。
另外,有一个需要注意的地方就是,不能将函数的 arguments 泄露或者传递出去。如果将arguments对象泄漏出去了,最终的结果就是V8引擎将会跳过优化,导致相当大的性能损失。
修改arguments的值
这要分严格模式,和非严格模式。
在严格模式下,参数与arguments对象没有联系,修改一个值不会改变另一个值。而在非严格模式下两个会互相影响。
如果参数没有传入,失去绑定关系
用arguments模拟重载
1 | function add(num1, num2, num3) { |
caller 与 callee
讲到arguments,就顺带讲一下caller和callee
caller
返回调用指定函数的函数,如果是在全局作用内调用,则返回null。 如果一个函数是在另外一个函数作用域内被调用的,则f.caller指向调用它的那个函数。
但是arguments.caller已经被废弃,被arguments.callee.caller所代替。
callee
callee 是 arguments 对象的一个属性。它可以用于引用该函数的函数体内当前正在执行的函数。返回的就是fun函数本身。但在严格模式中不允许使用
我们还可以用它来验证传入的参数是否与形参相等
1 | function fun(fun1, fun2){ |
ES6 中的 arguments
- 扩展操作符
直接上栗子:1
2
3
4
5function func() {
console.log(...arguments);
}
func(1, 2, 3);
执行结果是:1
1 2 3
简洁地讲,扩展操作符可以将 arguments 展开成独立的参数。
- Rest 参数
还是上栗子:1
2
3
4
5
6function func(firstArg, ...restArgs) {
console.log(Array.isArray(restArgs));
console.log(firstArg, restArgs);
}
func(1, 2, 3);
执行结果是:1
2true
1 [2, 3]
从上面的结果可以看出,Rest 参数表示除了明确指定剩下的参数集合,类型是 Array。
- 默认参数
栗子:1
2
3
4
5
6function func(firstArg = 0, secondArg = 1) {
console.log(arguments[0], arguments[1]);
console.log(firstArg, secondArg);
}
func(99);
执行结果是:1
299 undefined
99 1
可见,默认参数对 arguments 没有影响,arguments 还是仅仅表示调用函数时所传入的所有参数。
JS类型判断
typeof
用typeof可以检测基本类型和函数类型。
6大原始类型Null、Undefined、String、Number、Boolean和Symbol。
【注意】 typeof null 返回的是 object。
我们来谈谈为什么typeof null会返回object,这是一个历史遗留问题。
js 在底层存储变量的时候,会在变量的机器码的低位1-3位存储其类型信息
- 1:整型(int)
- 000:引用类型(object)
- 010:双精度浮点型(double)
- 100:字符串(string)
- 110:布尔型(boolean
另外还用两个特殊值: - undefined,用整数−2^30(负2的30次方,不在整型的范围内)
- null,机器码空指针(C/C++ 宏定义),低三位也是000
所以,typeof 在判断 null 的时候就出现问题了,由于 null 的所有机器码均为0,因此直接被当做了对象来看待。
instanceof
基于原型链进行判断,一般用于判断对象类型。
instanceof左操作数,为一个对象 如果不是对象会直接返回false。右操作数为一个函数构造器。
instanceof可以来判断一个实例是否属于某种类型,也可以判断一个实例是否是其父类型或者祖先类型的实例。
instanceof 主要的实现原理就是只要右边变量的prototype在左边变量的原型链上即可。因此instanceof在查找的过程中会遍历左边变量的原型链,直到找到右边变量的prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。
一般用于自定义对象实例判断。1
2
3
4
5
6
7
8let Person = function () {
}
let Programmer = function () {
}
Programmer.prototype = new Person()
let liam = new Programmer()
liam instanceof Person // true
liam instanceof Programmer // true
Object.prototype.toString
这是一个不错的判断类型的方法。但是要注意,在IE 6、7、8的时候, null和undefined会返回”[object Object]”。
1 | Object.prototype.toString.call(1) // "[object Number]" |
constructor
constructor含义就是指向该对象的构造函数,每个对象都有构造函数,有可能是本身拥有也有可能是继承而来。所有函数和对象最终都是由Function构造函数得来。
1 | const arr = [] |
定义一个数组arr,然后我们取arr的constructor
它本身没有constructor属性。
就从arr.proto中查找
arr.proto又指向Array.prototype
有因为Array.prototype.constructor === Array, 所以arr.constructor为Array
但是由于我们可以对原型的指向进行修改。所以检测结果有时并不准确。
duck type
通过特性嗅探来进行类型验证,比如Array类型,就通过判断是否有.sort或者.slice等等数组特有的方法来判断是否为数组。
JS对数组的判断
这个一般会单独拿出来讲,数组中内置了一个数据类型判断函数isArray,但会有兼容性问题。所以一般的写法如下:
1 | isArray = Array.isArray || function(array){ |
JS递归
我们知道JS中实现一个递归跟别的语言一样还是很容易的。1
2
3function fn() {
return fn();
}
有一些问题,函数是在定以后绑定到fn这个变量上,如果解释器先解析函数体,再把函数绑到变量上,在解析函数体的时候fn这个变量还处于未定义。
进阶:用匿名函数来实现递归,这时就需要借助callee1
2
3function (n) {
return arguments.callee(n - 1);
}
要注意的是,这个在严格模式下不能使用。
Object.toString和Object.prototype.toString之间的区别
我们要注意Object.proto === Function.prototype;因为proto指向的是构造该对象的构造函数的原型。
而构造函数除了是方法它也是对象,而函数的构造函数就是Function,所以Ojbect.proto指向的就是Function.prototype。 而Function.prototype又是对象,它的构造函数就是Object,因此Function.prototype.proto指向的就是Object.prototype。
我们知道,Object本身是没有toString方法的,它会去它的原型链上去找,也就是proto上,于是。就找到了Function.prototype.toString 而不是 Object.prototype.toString。
一般对象都会在原型上实现自己的toString方法。所以源码中利用toString方法来判断是否是一个真正的Object对象。
for in陷阱
我们先来看看for in的定义,以任意顺序遍历一个对象的除Symbol以外的可枚举属性。
首先我们可以知道,一、for in是以任意顺序遍历;二、它遍历可枚举的属性,要注意的是它也会遍历原型链上可枚举的属性。
我们可以使用对象的hasOwnProperty()方法来避免这个问题。而在使用hasOwnProperty()的时候就引出了下面的问题。
为什么不直接用hasOwnProperty判断?
这很简单,因为 JS 不保护属性名hasOwnProperty,你可以轻松地重写它,就像这样。1
2
3
4
5var foo = {
hasOwnProperty: function() {
return false;
},
};
所以我们在使用hasOwnProperty的时候这样来使用它。({}).hasOwnProperty.call
或者 Object.prototype.hasOwnProperty.call