抛弃 jQuery 之后的其它补充
这是本系列的最后一篇文章了,之前讲的选择元素,DOM 操作,Ajax 处理,事件处理已涵盖了大部分,但是 jQuery 并非如此简单,它还内置了很多其它方法。同样本篇文章讲的大多还是要基于高级浏览器(IE 9+),可以视作 HTML5 环境吧。
判断是否是对象,数组,函数
虽然这个需求并不太常用,但还是会有碰到的时候,比如在函数里检测传入的参数。如果是使用 jQuery 实现,则方面很多,如下:
$.isFunction(value);
//是否是函数
$.isPlainObject(value);
//是否是对象
$.isArray(value);
//是否是数组
上面这三个是 jQuery 内置的方法,返回的是个布尔值。注意第二个,是 isPlainObject 而不是 isObject 。中间多了个 Plain,纯的意思,因为在 JavaScript 里,函数和数组的类型都是 Object。
如使用纯 JavaScript 可以使用 typeof
来判断一个值是不是函数:
typeof value === 'function';
如果判断一个对象,单单使用 typeof 是不够的,因为:
typeof null
//返回 object
typeof []
//返回 object
于此为了精确判断,必须使用 Object.toString()
加持一下。如下:
value != null && Object.prototype.toString.call(value) === '[object Object]';
如果要判断一个值是否是一个数组,原理一样,但高级浏览器有内置了一个方法,如下:
Array.isArray(value);
//只对高级浏览器
Object.prototype.toString.call(value) === '[object Array]';
//全部浏览器
合并与拷贝对象
假设有两个对象,o1 和 o2。
var o1 = {
a: 'a',
b: {
b1: 'b1'
}
},
o2 = {
b: {
b2: 'b2'
},
c: 'c'
};
如果要把 o2 合并到 o1 ,那么结果应该是这样的:
{
a: 'a',
b: {
b1: 'b1',
b2: 'b2'
},
c: 'c'
}
如果使用 jQuery 操作,使用 $.extend()
方法,如下:
$.extend(true, o1, o2);
//把 o2 合并到 o1
var newObj = $.extend(true, {}, o1, o2);
// o2 合到 o1 再合到一个空对象,返回一个新对象
上述的第一个参数 true
,意指是否深度拷贝。深拷贝和浅拷贝的区别在于内存地址指向,如果只是 var newObj = o2
, 那么 newObj
只是把指向指到 o2
的内存地址,如果 o2
改变了,newObj
也会跟着改变,这是浅拷贝,显然大多时候不能满足需求。而深拷贝是新建一个内存地址,并把原来的值拷贝过来,o2
改变了,newObj
不会跟着变。于此,使用 jQuery 拷贝一个对象,写法如下:
var copyOfO1 = $.extend(true, {}, o1);
如果使用纯 javaScript 合并对象,我们得先写个 extend 函数,中心思想是遍历对象和递归调用,如下:
function extend(first, second) {
for (var secondProp in second) {
var secondVal = second[secondProp];
// Is this value an object? If so, iterate over its properties, copying them over
if (secondVal && Object.prototype.toString.call(secondVal) === "[object Object]") {
first[secondProp] = first[secondProp] || {};
extend(first[secondProp], secondVal);
}
else {
first[secondProp] = secondVal;
}
}
return first;
};
extend(o1, o2);
//把 o2 合并到 o1
var newObj = extend(extend({}, o1), o2);
// o1 先与空对象合并,返回一个新对象,o2 再与这个新对象合并,最后返回一个新对象
如果只是为了拷贝一个对象,其实只是少了个合并而已,拷贝 o1 如下:
var copyOfO1 = extend({}, o1);
遍历对象属性
假设我们有一个对象,称之为 parentObject
,如下:
var parentObject = {
a: 'a',
b: 'b'
};
然后我们在创建一个对象 myObject
,并且继承 parentObject
:
var myObject = Object.create(parentObject);
myObject.c = "c";
myObject.d = "d";
那么这个时候,myObject
就有 4 个属性了,但是只有 c 和 d 是自己的,a 和 b 是继承自 parentObject
。因为它们在同一条原型链上。那么此时我们要遍历 myObject
的属性,但是呢,我们只需要属于 myObject
的那部分属性,而不是所有属性。使用 jQuery 的话,可以用 $.each()
实现:
$.each(myObject, function(propName, propValue) {
// 处理逻辑
});
上述代码,会遍历 myObject
的所有属性,但是会忽略不直属于 myObject
的属性,也就是遍历出来的只有 c 和 d。
如果使用纯 javaScript 遍历一个对象,我们可以用 for...in
循环,如下:
for (var myObjectProp in myObject) {
// 处理逻辑
}
但是,但是如果只使用 for...in
会把继承的属性也循环出来,也就是遍历出来的属性有四个,a、b、c、d。显然不合适,此时必须使用 Object.hasOwnProperty()
加持,如下:
for (var prop in myObject) {
if (myObject.hasOwnProperty(prop)) {
// 逻辑处理,只有 c 和 d 会进入到这
}
}
以上代码兼容所有浏览器,但看起来略复杂,如果使用高级浏览器,则有更简便的方法,因为 ECMAScript 5 定义了一个新方法:Object.keys(someObj)
。会把传入的对象的属性以数组的形式返回,同样不包含继承的属性,如下:
Object.keys(myObject).forEach(function(prop) {
// 逻辑处理,只有 c 和 d 会进入到这
});
以上代码只适用于高级浏览器。
遍历数组
我们有一个数组如下:
var myArray = ['a', 'b', 'c'];
如使用 jQuery 遍历,同样使用 $.each()
,如下:
$.each(myArray, function(index, value) {
// 逻辑处理
});
如使用纯 JavaScript 处理,可以使用 for
循环,如下:
for (var i = 0; i < myArray.length; i++) {
var arrayVal = myrray[i];
// 逻辑处理
}
以上代码兼容所有浏览器,但是和 jQuery 的比起来,显然不够优雅,如果是高级浏览器,则可以使用 Array.prototype.forEach
这个方法,如下:
myArray.forEach(function(value, index) {
// 逻辑处理
}
以上代码,注意一点,传入的函数的参数,value
在前,index
在后,跟 jQuery 的正好相反,个人喜欢这个顺序。jQuery 把 index
放在前面,太反人类了。
查找数组中的元素
先设定一个简单的数组,如下:
var theArray = ['a', 'b', 'c'];
如果要查找某个元素在数组中的索引值,使用 jQuery 如下:
var indexOfValue = $.inArray('c', theArray);
以上的 indexOfValue
为 2,因为索引值是从 0 开始算的。如果说查找的元素不在数组里,则索引值会返回 -1 ,这个也是判断某个数组是否含有某个元素的做法。
如果要把数组中的某些元素过滤出来,比如在上述数组中,要把值不等于 b 的元素全部提取出来,可以使用 jQuery 的 grep
方法,如下:
var allElementsThatMatch = $.grep(theArray, function(theArrayValue) {
return theArrayValue !== 'b';
});
以上代码 allElementsThatMatch
会等于 [‘a’, ‘c’] 。
如果使用纯 JavaScript 来查找数组中的某个元素,在高级浏览器可以使用 Array.prototype.indexOf
这个方法,如下:
var indexOfValue = theArray.indexOf('c');
比较遗憾的是,这个方法在低级浏览器并不含有,比如 IE 8,那只能使用 for
循环了。如下:
function indexOf(array, valToFind) {
var foundIndex = -1;
for (var index = 0; index < array.length; index++) {
if (array[index] === valToFind) {
foundIndex = index;
break;
}
}
return foundIndex;
}
indexOf(theArray, 'c');
// 返回 2
如果是要过滤元素,在高级浏览器可以使用 Array.prototype.filter
这个方法,如下:
var allElementsThatMatch = theArray.filter(function(theArrayValue) {
return theArrayValue !== 'b';
});
这个写法跟 jQuery 的 grep
很像了,如果是为了照顾低级浏览器,还是要使用 for
循环,如下:
function filter(array, conditionFunction) {
var validValues = [];
for (var index = 0; index < array.length; i++) {
if (conditionFunction(theArray[index])) {
validValues.push(theArray[index]);
}
}
}
var allElementsThatMatch = filter(theArray, function(arrayVal) {
return arrayVal !== 'b';
})
// 返回 ['a', 'c']
把伪数组变成真数组
在 JavaScript 里,真数组是这样的:
var realArray = ['a', 'b', 'c'];
那么啥是伪数组?如下:
var pseudoArray1 = {
1: 'a',
2: 'b',
3: 'c'
length: 3
};
这个伪数组 key
和真数组一样,并且还有一个 length
属性。这是一个人造的伪数组,如果不人造呢,内置的有 NodeList ,HTMLCollection 还有 FileList。它们和数组很像,但它们又不是数组,以至于没办法用数组的方法进行操作。实际上它们是一个对象,但是又具备了 length
属性,所以我们可以把它们转化成数组,再用数组的方法进行操作。
如果是使用低级浏览器,到没有转换成真数组的必要,因为操作都要用 for
循环啊!如果是高级浏览器,希望使用数组的 forEach
,map
,filter
这些方法,那么就先要转化成真的数组了,使用 jQuery,可以用其之 $.makeArray()
实现,如下:
var realArray = $.makeArray(pseudoArray);
如果使用纯 JavaScript 实现如下:
var realArray = [].slice.call(pseudoArray);
如果只是为了把某个数组方法用在伪数组上,可以更简单,实现如下:
[].forEach.call(pseudoArray, function(arrayValue) {
// 逻辑处理
});
改变函数的上下文
函数的上下文,主要指该函数执行时,函数内部 this
的指向。比如有如下函数:
function Outer() {
var eventHandler = function() {
this.foo = 'buzz';
};
this.foo = 'bar';
//定义 foo
eventHandler();
//重新定义 foo
}
var outer = new Outer();
在这个函数中,outer
等于 {foo: "bar"}
,很明显 eventHandler()
并没有把 this.foo
的值重新定义,而我们需要的是 outer.foo === 'buzz'
。为什么?通过 console.log()
打印 this
可以发现 eventHandler()
里面的 this
指向是全局 window
,而外面的 this
指向的是 Outer()
本身。导致执行重新定义的时候,相当于给 window 增加了一个 foo
的属性,值为 buzz
。此时我们就需要给 eventHandler() 指定一个上下文了。可以使用 jQuery 的 $.proxy()
实现,如下:
function Outer() {
var eventHandler = $.proxy(function() {
this.foo = 'buzz';
}, this);
this.foo = 'bar';
//定义 foo
eventHandler();
//重新定义 foo
var outer = new Outer();
注意 $.proxy()
的第二个参数是 this
,而这个 this
因为是在外面,所以指到了 Outer()
本身,所以最后 outer
等于 {foo: "buzz"}
,符合我们的需求。那么使用原生的 JavaScript 要如何实现?如果是高级浏览器,有内置了一个 Function.prototype.bind() 方法,实现起来和 jQuery 很像,如下:
function Outer() {
var eventHandler = function() {
this.foo = 'buzz';
}.bind(this);
this.foo = 'bar';
//定义 foo
eventHandler();
//重新定义 foo
var outer = new Outer();
注意 eventHandler()
最后的 .bind(this)
,由此加持,最后 outer
等于 {foo: "buzz"}
符合需求。如果是 IE 8 以及以下浏览器,实现的方式其实也可以说挺方便的,如下:
function Outer() {
var self = this,
eventHandler = function() {
self.foo = 'buzz';
};
this.foo = 'bar';
//定义 foo
eventHandler();
//重新定义 foo
var outer = new Outer();
重点在于 var self = this
这句,把外层函数的 this
定义成一个变量,然后内嵌函数以此操作。
清除字符串前后空格
价格有一个字符串,如下
" hi there! "
现在我们要把前后的空格删除掉,结果需要如下:
"hi there!"
如果使用 jQuery 可以用其内置的 $.trim()
方法,如下:
$.trim(' hi there! ');
如果是高级浏览器,其在 String
原型上也有内置的 trim()
方法,如下:
' hi there! '.trim();
如果是低级浏览器,就没啥方法,不过可以使用正则表达式替换成空,如下:
' hi there! '.replace(/^\s+|\s+$/g, '');
当然我们可以把二者结合起来,自己封装一个 trim 函数,以达到兼容效果,需要的话:
function trim(string) {
if (string.trim) {
return string.trim();
}
return string.replace(/^\s+|\s+$/g, '');
}
把数据关联到 HTML 元素
把数据“关联”到元素的方法,最为直接的方法,作为属性添加到其标签上,但显然这样操作起来非常有限,完全应付不了复杂的数据,并且不安全,所以必须附属在内存上,而非标签本身。这样可以附属任何类型的数据,避免了循环引用而导致内存泄漏。
比方说,我们有两个元素:
<div id="one">one</div>
<div id="two">two</div>
当我们点击其中一个,另一个元素在隐藏与显示之间切换,我们用 jQuery 的 $.data()
实现,如下:
$('#one').data('partnerElement', $('#two'));
$('#two').data('partnerElement', $('#one'));
$('#one, #two').click(function() {
$(this).data('partnerElement').toggle();
});
运行效果可以看这里 https://appblur.com/demos/not-jquery/html-data-1.html。jQuery 的 $.data()
会为每个需要关联数据的元素创建一个 ID,接着把这些 ID 作为属性集合到该元素的 JavaScript 对象上,然后使用这个 ID 去中心对象(jQuery 创建,数据统一储存在这)操作对应的值。用图表示:
如果使用原生 JavaScript 实现这个功能呢?代码有点多:
var data = (function() {
var lastId = 0,
store = {};
return {
set: function(element, info) {
var id;
if (element.myCustomDataTag === undefined) {
id = lastId++;
element.myCustomDataTag = id;
}
store[id] = info;
},
get: function(element) {
return store[element.myCustomDataTag];
}
};
}());
var one = document.getElementById('one'),
two = document.getElementById('two'),
toggle = function(element) {
if (element.style.display !== 'none') {
element.style.display = 'none';
}
else {
element.style.display = 'block';
}
};
data.set(one, {partnerElement: two});
data.set(two, {partnerElement: one});
one.addEventListener('click', function() {
toggle(data.get(one).partnerElement);
});
two.addEventListener('click', function() {
toggle(data.get(two).partnerElement);
});
运行效果可以看这里 https://appblur.com/demos/not-jquery/html-data-2.html。这段代码可以在所有的浏览器上运行,思路是,先创建两个函数,一个用于把 ID 关联到元素和关联到存储数据的中心对象,另一个函数负责检索数据。讲真,这一段和 jQuery 比起来实在太糟糕了,像个地狱一样。但是记住,这是运行在所有浏览器上的,所以略显复杂。但是浏览器和 JavaScript 都一直在更新,很多方式实现起来会越来越简单,ECMAScript 6 内置了一个全新的对象 <a href=“https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/WeakMap” target=“_blank”>WeakMap”</a>,WeakMap 就是简单的键/值映射,但键只能是对象值,不可以是原始值。关于支持情况,WeakMap 支持的偏更高端:IE 11+,Chrome 36+, Safari 7.1 和 Firefox 6+。如果使用该对象实现上述功能,如下:
var weakMap = new WeakMap(),
one = document.getElementById('one'),
two = document.getElementById('two'),
toggle = function(element) {
if (element.style.display !== 'none') {
element.style.display = 'none';
}
else {
element.style.display = 'block';
}
};
weakMap.set(one, {partnerElement: two});
weakMap.set(two, {partnerElement: one});
one.addEventListener('click', function() {
toggle(weakMap.get(one).partnerElement);
});
two.addEventListener('click', function() {
toggle(weakMap.get(two).partnerElement);
});
比较尴尬的是 weakMap
对 IE 的支持实在太差了,不过没关系,我们可以自己搞个 Polyfill,如下:
var data = window.WeakMap ? new WeakMap() : (function() {
var lastId = 0,
store = {};
return {
set: function(element, info) {
var id;
if (element.myCustomDataTag === undefined) {
id = lastId++;
element.myCustomDataTag = id;
}
store[id] = info;
},
get: function(element) {
return store[element.myCustomDataTag];
}
};
}());
这段代码可以在所有的浏览器工作,请放心使用。
One More Thing
《或许你不再需要使用 jQuery 了》 这个系列,到此结束。希望阁下能够多少受益,天空多灰,我们亦放亮…