I’m going to dub this a featurefill as the post title suggests, but it’s more a feature-detect-closure-binding-smart-polyfill-api-checker-reusable-function-awesomeness.
So, what’s the deal?… I’m a huge fan of polyfilling behaviour for older browsers that don’t support specific APIs, such as Function.prototype.bind
or Array.prototype.forEach
. Typically, we’d drop these polyfills into our apps like so:
Table of contents
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError(
'Function.prototype.bind - what is trying to be bound is not callable'
);
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function() {},
fBound = function() {
return fToBind.apply(
this instanceof fNOP && oThis ? this : oThis,
aArgs.concat(Array.prototype.slice.call(arguments))
);
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
Then we can get on with our development and start using Function.prototype.bind
until our heart’s content.
The thing I’m questioning though, is this a good way to do things? Our if
statement checks for the existence of a method on the prototype
chain, and if it’s not there it patches it.
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
I’m thinking there may be better ways to do this. Instead of checking for something that doesn’t exist, and hope there are no other polyfills that create strange behaviour from modifying our Objects, we could wrap the contents of our polyfill inside a clever closure, and return if the API does exist, rather than if it doesn’t.
This would require us to construct our own methods, however, but it tightly packs the functionality for solid code reuse in later projects.
A quick starting function to demonstrate the concept, we’ll create an isArray
method, instead of polyfilling the ES5 method:
function isArray(collection) {}
Let’s check out the polyfill for the isArray
method:
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
Again, it detects for the presence of the method and creates it for us if it doesn’t exist. Let’s detect if the method does exist, and get smarter with our polyfilling by using the native methods when available first.
function isArray(collection) {
if (Array.isArray) {
return Array.isArray(collection);
}
}
We don’t need an else
statement because we’ll have already returned if the method exists, so we’ll drop a modified version of the above !Array.isArray
polyfill in:
function isArray(collection) {
if (Array.isArray) {
return Array.isArray(collection);
}
return Object.prototype.toString.call(collection) === '[object Array]';
}
Done. Simple! This uses the native method when available, and gracefully falls back to a polyfill if it’s not.
There is one slight problem with this approach, however, in that the if
statement is checked every time the function is called. We’ll use a closure to return only the things we need at runtime, to increase the performance of multiple checks.
First, we’ll switch the isArray
function for a variable:
var isArray;
Then assign an IIFE:
var isArray = (function() {})();
This function executes immediately, so we can return a value to it so it’s bound for the lifetime of the program. Should Array.isArray
be natively available, let’s give that back to the developer:
var isArray = (function() {
if (Array.isArray) {
return Array.isArray;
}
})();
Notice we don’t now have to call Array.isArray(collection);
, we just return the native API. The polyfill requires returning a function closure with our collection
argument, which then returns what we need for the polyfill:
var isArray = (function() {
if (Array.isArray) {
return Array.isArray;
}
return function(collection) {
return Object.prototype.toString.call(collection) === '[object Array]';
};
})();
If our browser supports the Array.isArray
method then we’ll actually get this (if we logged it out in the console):
function isArray() { [native code] }
If our browser doesn’t support it, we get the patch:
function (collection) {
return Object.prototype.toString.call(collection) === '[object Array]';
}
This is great, as it means that we’re actually getting a single return value bound to the variable, so there’s no if
checking for API presence and we should get some performance gains from this.
If you’re running test suites on your functions, baking your own library, then using this method I’d highly recommend over dropping in random polyfills. It provides expected behaviour that doesn’t modify the existence of APIs that might or might not be there.