Utilities
Is this an Object, Array, or a Function?
jQuery:
$.isFunction(someValue);
$.isPlainObject(someValue);
$.isArray(someValue);
vanilla JavaScript:
// is this a function?
typeof someValue === 'function';
// is this an object?
someValue != null &&
Object.prototype.toString.call(someValue) === "[object Object]";
// works in modern browsers
Array.isArray(someValue);
// works in all browsers
Object.prototype.toString.call(someValue) === "[object Array]";
Combine and copy objects
var o1 = {
a: 'a',
b: {
b1: 'b1'
}
},
o2 = {
b: {
b2: 'b2'
},
c: 'c'
};
现在你想要两个对象变成:
{
a: 'a',
b: {
b1: 'b1',
b2: 'b2'
},
c: 'c'
}
jQuery
// updates o1 with the contents of o2
$.extend(true, o1, o2);
// creates a new object that is the aggregate of o1 & o2
var newObj = $.extend(true, {}, o1, o2);
If we just want to create a copy of one of the objects, we can do that too, also using $.extend:
var copyOfO1 = $.extend(true, {}, o1);
vanilla JavaScript:
// our helper function
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;
};
// example use - updates o1 with the content of o2
extend(o1, o2);
// example use - creates a new object that is the aggregate of o1 & o2
var newObj = extend(extend({}, o1), o2);
Iterate over object properties
Let's say we have the following Object, called parentObject:
var parentObject = { a: 'a', b: 'b' }; ...and then we create myObject, which inherits the properties of parentObject:
var myObject = Object.create(parentObject);
myObject.c = "c";
myObject.d = "d";
Now, myObject has 4 properties, but only 'c' and 'd' belong to myObject (只有 c 和 d 属于 myObject). Properties 'a' and 'b' are part of parentObject, which is on the same prototype chain ( a 和 b 位于原型链上 ) as myObject. Most likely, when we want to iterate over the properties of myObject, we only want the properties that belong to this object ( 我们只想遍历属于自己的属性 ), and not any that belong to parentObject.
jQuery
$.each(myObject, function(propName, propValue) {
// handle each property...
});
The above code will iterate over all of the myObject properties, ignoring any properties that do not directly belong to myObject (忽略所有不直接属于 myObject 的属性).
vanilla JavaScript
In the previous section, while discussing copying and combining objects, I included a barebones method of iterating over an object's properties. In case you need to deal with something more complex or want to exploit native approaches available in modern browsers, I'll go over some other approaches, as well as some things you should be aware of when dealing with object property iteration.
For reference, here is the simple for...in loop presented in the "Combine & Copy Objects" section, now iterating over myObject:
for (var myObjectProp in myObject) {
// deal with each property in the `myObject` object...
}
So, if we outputted every property encountered by the loop above, we'd see the properties on myObject, as well as the properties of parentObject. Remember, parentObject and myObject are on the same prototype chain, and myObject inherits all of the properties from parentObject. But, this is not what we want. We only want the properties that myObject owns! To acheive this, we need to exclude any other properties, and this can be done via the hasOwnProperty
method.
// works in all browsers
for (var prop in myObject) {
if (myObject.hasOwnProperty(prop)) {
// deal w/ each property that belongs only to `myObject`...
}
}
All objects have a hasOwnProperty
method, as this method is available on Object.prototype
.
But wait! There's another solution, specific to modern browsers. ECMAScript 5 defines a new function: Object.keys(someObj)
, which returns an array of properties on the passed object. Also, this array will only contain the properties owned by the passed object (只包含属于自己的那些属性).
// works in modern browsers
Object.keys(myObject).forEach(function(prop) {
// deal with each property that belongs to `myObject`...
});
Note that I'm using forEach on the returned array of property names above. I'll talk more about iterating over arrays in the next section.
Iterate over array elements
Given an array, like this:
var myArray = ['a', 'b', 'c']; ...we'd like to be able to simply iterate over each of the elements in this array.
jQuery
Iterating over arrays in jQuery is a lot like iterating over object properties. In both cases, just use the jQuery.each method:
$.each(myArray, function(arrayIndex, arrayValue) {
// handle each array item...
});
vanilla JavaScript
This isn't much harder without jQuery, regardless of the browser. We can simply iterate over the elements in the array using a simple for loop:
// works in all browsers
for (var index = 0; i < myArray.length; i++) {
var arrayVal = myrray[index];
// handle each array item...
}
But that doesn't look nearly as nice as jQuery, does it? ECMAScript 5 to the rescue again! We can make use of Array.prototype.forEach
in modern browsers instead:
// works in modern browsers
myArray.forEach(function(arrayVal, arrayIndex) {
// handle each array item
}
Find an element in an array
Here's I'll cover two similar operations on an array:
- Find the index of an array that contains a specific value.
- Find all elements in an array that satisfy a particular condition (满足一定条件).
Let's again use a simple array:
var theArray = ['a', 'b', 'c'];
jQuery
To find the index of an array for a specific value, we can use the inArray function:
var indexOfValue = $.inArray('c', theArray);
indexOfValue will be -1 if the value represented by the first parameter, 'c', is not found in theArray. Otherwise, it will be the index of theArray where the passed value was found. In this case, that index is 2.
If we want to use jQuery to find all elements in an array that satisfy a particular condition, we can use the grep
function:
var allElementsThatMatch = $.grep(theArray, function(theArrayValue) {
return theArrayValue !== 'b';
});
allElementsThatMatch will equal ['a', 'c'] as our filter function excludes any value that matches 'b'.
vanilla JavaScript
If you'd like to find the index of an array for a specific value, AND you are using a modern browser, you can make use of the Array.prototype.indexOf
method:
// works in modern browsers
var indexOfValue = theArray.indexOf('c');
Sadly, the indexOf method didn't come about until ECMAScript 5. So, if you are suffering with a requirement to support an ancient browser, such as IE 8, you must iterate over the elements of the array, using a for loop, until you find the desired element. For example:
// works in all browsers
function indexOf(array, valToFind) {
var foundIndex = -1;
for (var index = 0; index < array.length; index++) {
if (array[index] === valToFind) {
foundIndex = index;
break;
}
}
return foundIndex;
}
// example use
indexOf(theArray, 'c');
If you want to find all values that satisfy a specific condition in vanilla JS, you can make use of the shiny new Array.prototype.filter
method in ES5:
// works in modern browsers
var allElementsThatMatch = theArray.filter(function(theArrayValue) {
return theArrayValue !== 'b';
});
That actually looks a bit nicer than jQuery's grep method!
If you need to deal with ancient browsers, such as IE 8, you'll need to write a bit more code:
// works in all browsers
function filter(array, conditionFunction) {
var validValues = [];
for (var index = 0; index < array.length; i++) {
if (conditionFunction(theArray[index])) {
validValues.push(theArray[index]);
}
}
}
// use example
var allElementsThatMatch = filter(theArray, function(arrayVal) {
return arrayVal !== 'b';
})
Turn a pseudo-array into a real array
In JavaScript, there are real arrays:
var realArray = ['a', 'b', 'c'];
...and "pseudo"-arrays:
var pseudoArray1 = {
1: 'a',
2: 'b',
3: 'c'
length: 3
};
There are some native pseudo-arrays as well, such as NodeList, HTMLCollection, and FileList, to name a few. These are not real arrays in that they are not on the same prototype chain as Array. That is, they inherit nothing from Array.prototype because they are not arrays. In fact, they are just objects. But, due to the length property, you can treat them as an array, in some respects, by iterating over their "elements" using a for loop, just as you would any real array.
If you are dealing with a pseudo-array in an ancient browser, such as IE 8, it probably doesn't matter much, since you'll need iterate over the properties/elements using a for loop whether it is a real array or not. But, suppose you are using a modern browser, and you want to make this object that looks very much like an array act like an array. Perhaps you want to use forEach, or map, or filter. Or perhaps you must pass it to an API method that expects a real array.
jQuery
You can convert a pseudo-array to a real array in jQuery using the makeArray method:
var realArray = $.makeArray(pseudoArray);
vanilla JavaScript
If you'd like to transform a pseudo-array to a real array, and make all methods on Array.prototype available, you can use this trick:
var realArray = [].slice.call(pseudoArray);
If you only care to use a specific array method on a pseudo-array, such as forEach you can do this instead:
[].forEach.call(pseudoArray, function(arrayValue) {
// handle each element in the pseudo-array...
});
Modify a function
There are two ways to modify an existing function:
- Change its context.
- Create a new function with some pre-determined arguments.
In both cases, you would want to take an existing function and create a new function based on the original.
1 - Change a function's context
To demonstrate #1, let's propose we have an event handler. By default, the context of an event handler in modern browsers and jQuery is the element that started the event (except for inline event handlers, which are a bad idea anyway). But what if you want your event handler to assume a different context? For example, say you have some public instance variables attached to the context of a function, and you want to have easy access to these variables inside your event handler:
function Outer() {
var eventHandler = function(event) {
this.foo = 'buzz';
}
this.foo = 'bar';
// attach `eventHandler`...
}
var outer = new Outer();
// event is fired, triggering `eventHandler`
Without any changes, outer.foo === 'bar'. Since the foo property is being set in the context of an event handler, the element that triggered the event is actually receiving a foo property with a value of 'buzz'. But we want outer.foo === 'buzz'!
jQuery
It is possible to re-assign the context of a function in jQuery using the proxy function. To make this work, we simply need to wrap the function in a call to jQuery's proxy
utility method:
function Outer() {
var eventHandler = $.proxy(function(event) {
this.foo = 'buzz';
}, this);
this.foo = 'bar';
// attach `eventHandler`...
}
var outer = new Outer();
// event is fired, triggering `eventHandler`
Now, when the event handler is triggered, outer.foo === 'buzz'.
vanilla JavaScript
Modern browsers provide a bind function on Function.prototype
. This allows you to change the context of a function, just like jQuery's proxy, but in a slightly more elegant way:
// works in modern browsers
function Outer() {
var eventHandler = function(event) {
this.foo = 'buzz';
}.bind(this);
this.foo = 'bar';
// attach `eventHandler`...
}
var outer = new Outer();
// event is fired, triggering `eventHandler`
We simply call bind on the function, pass the desired context, and the resulting function assigned to eventHandler is associated with the context of our Outer function, which means outer.foo === 'buzz' when the event handler is ultimately invoked inside our Outer instance.
If you must support an ancient browser, like IE 8, you'll need to take one of two alternate approaches (支持 IE8,有两个方法). The first involves adding an implementation of bind to Function.prototype if the browser does not provide a native implementation (such as with IE 8 and older). Mozilla Developer Network provides some code for such a polyfill. I'm not going to repeat the code here, as it is displayed quite nicely on MDN. Simply invoking that function on page load will ensure that a bind utility function will be available for all functions. Another option is to take a completely different approach, and store the value of this in a variable inside of the Outer function. You can then reference that variable (instead of this) inside of eventHandler to ensure the correct property is updated. For instance:
// works in all browsers
function Outer() {
var self = this,
eventHandler = function(event) {
self.foo = 'buzz';
};
this.foo = 'bar';
// attach `eventHandler`...
}
var outer = new Outer();
// event is fired, triggering `eventHandler`
When the eventHandler is invoked, our instance of Outer, represented by the outer variable, will contain the expected value of foo, which is 'buzz'. This is a feasible solution in all browsers.
2 - Create a new function with some pre-determined arguments
Another way to phrase the method demonstrated here is "partial function application" or "currying". Instead of silly pointless examples involving functions that add numbers together, let's try to demonstrate something at least a bit more realistic.
Say we have a utility function that is part of a shared library used among many of our applications. This function logs messages to a remote server for aggregation and further evaluation. The function takes an application ID, a level (such as "info" or "error"), and a log message. It looks like this:
function log(appId, level, message) {
// send message to central server
}
We've pulled in the shared library containing this function into our app, and our application has an ID of "master-shake". Now, every time we log a message in our app, do we really want to pass the same first parameter, our app ID, which never changes? We certainly could, or, we can create a new function that fills in this parameter automatically for us. In other words, we want to call a function and only pass in the variable data: the level and message, with the same end result.
jQuery
As before, we'll use jQuery's proxy function:
var ourLog = $.proxy(log, null, 'master-shake');
// example use
ourLog('error', 'dancing is forbidden!');
Invoking ourLog will more or less pass the application ID ("master-shake"), as well as the specified error and message, to the original function.
vanilla JavaScript
We can again use the bind function for modern browsers. As before, if an ancient browser is required, a shim can be used to help us out.
var ourLog = log.bind(null, 'master-shake');
// example use
ourLog('error', 'dancing is forbidden!');
You're probably wondering about the significance of the null parameter we've passed to jQuery's proxy and native JS's bind function. We don't care much about context in this example, but passing null as the context to bind will set the value of this inside the bound function to the global function (window if we're in a browser). In the case of jQuery's proxy function, a null context parameter (at least as of jQuery 1.9) means that the proxied function will assume a value of this equal to the context of the calling function.
Trim a string
Given this string:
" hi there! "
We want to trim all of the leading and trailing whitespace to produce this:
"hi there!"
jQuery
We can using jQuery's trim function to do this:
$.trim(' hi there! ');
The string, sans leading & trailing whitespace, will be returned by the function call above.
vanilla JavaScript
In modern browsers, we can use the trim function on String.prototype:
// works in modern browsers
' hi there! '.trim();
For ancient browsers, we aren't so lucky, and have to resort to a regular expression to remove the leading and trailing spaces:
// works in all browsers, but needed in IE 8 and older
' hi there! '.replace(/^\s+|\s+$/g, '');
You can roll this into a function that will use the trim method if it exists, else the regexp if we're dealing with an ancient browser:
// works in all browsers
function trim(string) {
if (string.trim) {
return string.trim();
}
return string.replace(/^\s+|\s+$/g, '');
}
Associate data with an HTML element
You may find yourself wishing to track data (objects, strings, numbers, and even other elements) in the context of a specific HTML element. The safest way to do this, cross browser, to avoid memory leaks associated with circular references between elements, is to not attach this data directly to the element's associated JavaScript object as a property. If you're only tracking string, number, or simply object values, then this isn't much of an issue, but once you involve elements as data values as well, older browsers may spring memory leaks. For more information on that, feel free to read Joel Webber's old but interesting points on the subject.
To keep things simple, straightforward, and safe, we're going to avoid storing anything other than strings and numbers directly on the element. So, we'll need to tag our elements with an ID/key, and attach that key to our data in an object maintained in a data store apart from the element.
Let's say we have two elements, and when we click on one, we want the other to be either hidden or made visible (the opposite of its current state).
<div id="one">one</div>
<div id="two">two</div>
This example is a bit contrived, admittedly, but it illustrates our topic rather simply.
jQuery
jQuery's data method will create an ID for each element we want to tag with data, add that ID as a property to the element's JavaScript object, and use that ID to tie the element to the data in a central object maintained by the library.
// make the elements aware of each other
$('#one').data('partnerElement', $('#two'));
$('#two').data('partnerElement', $('#one'));
// on click, either hide or show the partner element
$('#one, #two').click(function() {
$(this).data('partnerElement').toggle();
});
vanilla JavaScript
There are a couple ways to do this in vanilla JS. The first will work in all browsers, but requires a bit more code. For this first approach, let's create a couple functions. One will tag an element with an ID and store that ID along with the data in an object. The other will retrieve data, given an element:
// works in all browsers
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];
}
};
}());
...and now let's use it based on our previously stated requirements:
// make the elements aware of each other
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});
// on click, either hide or show the partner element
// remember to use `attachEvent` in IE 8 and older, if support is required
one.addEventListener('click', function() {
toggle(data.get(one).partnerElement);
});
two.addEventListener('click', function() {
toggle(data.get(two).partnerElement);
});
Yes, this is a hell of a lot more code than the jQuery example. I know. Remember, this is about understanding JavaScript, not necessarily about counting lines of code. But the awesome thing about JavaScript as a language is that it continues to evolve and make our lives easier. ECMAScript 6 brings a new collection, called a WeakMap. A WeakMap can contain keys that are objects, and values that are anything. Keys are "weakly" held by the collection. This means that they are eligible for garbage collection by the browser if nothing else references them. So, we can use the reference elements as keys!
While WeakMap is only supported in the latest and greatest browsers (IE 11+, Chrome 36+, Safari 7.1+) and Firefox 6+, we can perhaps make use of it if the browser provides such support. If we rely only on the WeakMap, we can eliminate our data helper entirely, and our entire set of code looks like this instead:
// works only in the latest browsers
// make the elements aware of each other
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});
// on click, either hide or show the partner element
// remember to use `attachEvent` in IE 8 and older, if support is required
one.addEventListener('click', function() {
toggle(weakMap.get(one).partnerElement);
});
two.addEventListener('click', function() {
toggle(weakMap.get(two).partnerElement);
});
Of course, we can't rely entirely on WeakMap, yet (unless we only support IE 11+). So, we'll probably instead want to continue to use our data provider, relying on WeakMap only if the browser provides an implementation:
// works in all browsers
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];
}
};
}());