0%

通过Classnames源码学习基础知识

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

我们可以先来简单地看一下它的用法。

  1. 最基础的两个className拼接,接收多个className返回拼接好的字符串

    1
    classNames('foo', 'bar'); // => 'foo bar'
  2. 也可以接收对象,对象的key作为要拼接的className,key所对应的value为一个布尔值,来表示是否拼接。true拼接,false不拼接。一般用于控制一个元素的显隐。

    1
    2
    classNames('foo', { bar: true }); // => 'foo bar'
    classNames('foo'. { bar: false }); // => 'foo'
  3. 也可以传一个数组,就像这样:

    1
    2
    var arr = ['b', { c: true, d: false }];
    classNames('a', arr); // => 'a b c'
  4. 与ES6的字符串模板配合,使用动态class name:

    1
    2
    let 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
35
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) {
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;
这一行我们先不看。我们直接进入方法。

  1. 看第一个for循环:
    作用就是遍历这个方法传入的参数,然后判断参数,如果为falsy(如:null,undefined,false,’’)就直接返回。接下来就判断参数类型,根据类型分别进不同的判断条件。这里有两个知识点【arguments对象】和【JS的类型判断】。
  2. 第一个if条件很简单,如果传入参数为string类型或者number类型的话,就直接push到classes中,最后直接join(‘ ‘)即可。
  3. 第二个if,就是判断传入的参数是数组的时候。这里有一点,把判断为数组放在判断是否为对象之前因为type Array 返回的也是”object”,数组也是对象这很正常。这块主要就是递归调用这个方法。也是存在两个知识点【JS的数组判断】和【JS的递归实现
  4. 最后一个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
2
3
4
5
6
7
8
9
10
11
function add(num1, num2, num3) {
if (arguments.length === 2) {
console.log("Result is " + (num1 + num2));
}
else if (arguments.length === 3) {
console.log("Result is " + (num1 + num2 + num3));
}
}

add(1, 2);
add(1, 2, 3)

caller 与 callee

讲到arguments,就顺带讲一下caller和callee

caller

返回调用指定函数的函数,如果是在全局作用内调用,则返回null。 如果一个函数是在另外一个函数作用域内被调用的,则f.caller指向调用它的那个函数。
但是arguments.caller已经被废弃,被arguments.callee.caller所代替。

callee

callee 是 arguments 对象的一个属性。它可以用于引用该函数的函数体内当前正在执行的函数。返回的就是fun函数本身。但在严格模式中不允许使用

我们还可以用它来验证传入的参数是否与形参相等

1
2
3
4
5
6
7
function fun(fun1, fun2){
if(arguments.length===arguments.callee.length){//function对象的length属性返回的是参数个数
console.log("参数正确");
} else{
console.log("参数不正确");
}
}

ES6 中的 arguments

  1. 扩展操作符

直接上栗子:

1
2
3
4
5
function func() {
console.log(...arguments);
}

func(1, 2, 3);

执行结果是:

1
1 2 3

简洁地讲,扩展操作符可以将 arguments 展开成独立的参数。

  1. Rest 参数

还是上栗子:

1
2
3
4
5
6
function func(firstArg, ...restArgs) {
console.log(Array.isArray(restArgs));
console.log(firstArg, restArgs);
}

func(1, 2, 3);

执行结果是:

1
2
true   
1 [2, 3]

从上面的结果可以看出,Rest 参数表示除了明确指定剩下的参数集合,类型是 Array。

  1. 默认参数

栗子:

1
2
3
4
5
6
function func(firstArg = 0, secondArg = 1) {
console.log(arguments[0], arguments[1]);
console.log(firstArg, secondArg);
}

func(99);

执行结果是:

1
2
99 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. 1:整型(int)
  2. 000:引用类型(object)
  3. 010:双精度浮点型(double)
  4. 100:字符串(string)
  5. 110:布尔型(boolean
    另外还用两个特殊值:
  6. undefined,用整数−2^30(负2的30次方,不在整型的范围内)
  7. null,机器码空指针(C/C++ 宏定义),低三位也是000

所以,typeof 在判断 null 的时候就出现问题了,由于 null 的所有机器码均为0,因此直接被当做了对象来看待。

instanceof

基于原型链进行判断,一般用于判断对象类型。
instanceof左操作数,为一个对象 如果不是对象会直接返回false。右操作数为一个函数构造器。

instanceof可以来判断一个实例是否属于某种类型,也可以判断一个实例是否是其父类型或者祖先类型的实例。

instanceof 主要的实现原理就是只要右边变量的prototype在左边变量的原型链上即可。因此instanceof在查找的过程中会遍历左边变量的原型链,直到找到右边变量的prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。

一般用于自定义对象实例判断。

1
2
3
4
5
6
7
8
let 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Object.prototype.toString.call(1) // "[object Number]"

Object.prototype.toString.call('hi') // "[object String]"

Object.prototype.toString.call({a:'hi'}) // "[object Object]"

Object.prototype.toString.call([1,'a']) // "[object Array]"

Object.prototype.toString.call(true) // "[object Boolean]"

Object.prototype.toString.call(() => {}) // "[object Function]"

Object.prototype.toString.call(null) // "[object Null]"

Object.prototype.toString.call(undefined) // "[object Undefined]"

Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"

constructor

constructor含义就是指向该对象的构造函数,每个对象都有构造函数,有可能是本身拥有也有可能是继承而来。所有函数和对象最终都是由Function构造函数得来。

1
2
const arr = []
console.log(arr.constructor); // Array

定义一个数组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
2
3
isArray = Array.isArray || function(array){
return Object.prototype.toString.call(array) === '[object Array]';
}

JS递归

我们知道JS中实现一个递归跟别的语言一样还是很容易的。

1
2
3
function fn() {
return fn();
}

有一些问题,函数是在定以后绑定到fn这个变量上,如果解释器先解析函数体,再把函数绑到变量上,在解析函数体的时候fn这个变量还处于未定义。

进阶:用匿名函数来实现递归,这时就需要借助callee

1
2
3
function (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
5
var foo = {
hasOwnProperty: function() {
return false;
},
};

所以我们在使用hasOwnProperty的时候这样来使用它。
({}).hasOwnProperty.call 或者 Object.prototype.hasOwnProperty.call