Skip to content
Browse files

Detect non-native ES5 shims and fall back to our own.

This avoids potential issues with libraries that shim native objects
with broken implementations, like older Prototype and MooTools, and
certain Twitter widgets.

The technique used to detect native methods isn't perfect, but works in
all modern browsers and has minimal consequences if it fails.
  • Loading branch information...
1 parent 7649451 commit 3e4a3c30997cfa913fabd7b346e3a71388441bed @rgrove rgrove committed Jan 12, 2012
View
50 src/collection/HISTORY.md
@@ -1,67 +1,75 @@
Collection Change History
=========================
+3.5.0
+-----
+
+* YUI now detects non-native ES5 shims added to native objects by other
+ libraries and falls back to its own internal shims rather than relying on the
+ potentially broken code from the other library.
+
+
3.4.1
-----
- * Sparse arrays are now handled correctly in the non-native fallback
- implementation of `Y.Array.lastIndexOf()`. [Ticket #2530966]
+* Sparse arrays are now handled correctly in the non-native fallback
+ implementation of `Y.Array.lastIndexOf()`. [Ticket #2530966]
3.4.0
-----
- * Sparse arrays are now handled correctly in the non-native implementations of
- `Array.every`, `Array.filter`, `Array.find`, `Array.map`, and
- `Array.reduce`. [Ticket #2530376]
+* Sparse arrays are now handled correctly in the non-native implementations of
+ `Array.every`, `Array.filter`, `Array.find`, `Array.map`, and
+ `Array.reduce`. [Ticket #2530376]
3.3.0
-----
- * [!] The `sort` parameter of `Array.unique` has been deprecated. It still
- works, but you're encouraged not to use it as it will be removed from a
- future version of YUI.
- * `Array.lastIndexOf` now supports the `fromIndex` parameter as specified in
- ES5.
- * Improved the performance of `Array.filter`, `Array.map`, `Array.reduce`, and
- `Array.unique`, especially in browsers without native ES5 array extras.
+* [!] The `sort` parameter of `Array.unique` has been deprecated. It still
+ works, but you're encouraged not to use it as it will be removed from a
+ future version of YUI.
+* `Array.lastIndexOf` now supports the `fromIndex` parameter as specified in
+ ES5.
+* Improved the performance of `Array.filter`, `Array.map`, `Array.reduce`, and
+ `Array.unique`, especially in browsers without native ES5 array extras.
3.2.0
-----
- * No changes.
+* No changes.
3.1.1
-----
- * No changes.
+* No changes.
3.1.0
------
- * `array-extras` is the base submodule for the package.
- * Added `ArrayList` for generic iterable objects.
- * `Array.forEach` is an alias for `Array.each`.
- * Added `Array.invoke` to execute a named method on an array of objects.
+* `array-extras` is the base submodule for the package.
+* Added `ArrayList` for generic iterable objects.
+* `Array.forEach` is an alias for `Array.each`.
+* Added `Array.invoke` to execute a named method on an array of objects.
3.0.0
-----
- * `unique` with `sort` works.
+* `unique` with `sort` works.
3.0.0b1
-------
- * Fixed load time fork assumptions.
+* Fixed load time fork assumptions.
3.0.0pr1
--------
- * Initial release.
+* Initial release.
View
16 src/collection/js/array-extras.js
@@ -5,7 +5,9 @@ Adds additional utility methods to the `Y.Array` class.
@submodule array-extras
**/
-var L = Y.Lang, Native = Array.prototype, A = Y.Array;
+var A = Y.Array,
+ L = Y.Lang,
+ ArrayProto = Array.prototype;
/**
Returns the index of the last item in the array that contains the specified
@@ -23,7 +25,7 @@ value, or `-1` if the value isn't found.
@static
@for Array
**/
-A.lastIndexOf = Native.lastIndexOf ?
+A.lastIndexOf = L._isNative(ArrayProto.lastIndexOf) ?
function(a, val, fromIndex) {
// An undefined fromIndex is still considered a value by some (all?)
// native implementations, so we can't pass it unless it's actually
@@ -114,7 +116,7 @@ containing the items for which the supplied function returned a truthy value.
truthy value (empty if it never returned a truthy value).
@static
*/
-A.filter = Native.filter ?
+A.filter = L._isNative(ArrayProto.filter) ?
function(a, f, o) {
return a.filter(f, o);
} :
@@ -167,7 +169,7 @@ supplied function does not return a truthy value.
supplied function, `false` otherwise.
@static
*/
-A.every = Native.every ?
+A.every = L._isNative(ArrayProto.every) ?
function(a, f, o) {
return a.every(f, o);
} :
@@ -201,7 +203,7 @@ containing all the values returned by the supplied function.
for each item in the original array.
@static
*/
-A.map = Native.map ?
+A.map = L._isNative(ArrayProto.map) ?
function(a, f, o) {
return a.map(f, o);
} :
@@ -240,10 +242,10 @@ into a single value.
element in the array.
@static
*/
-A.reduce = Native.reduce ?
+A.reduce = L._isNative(ArrayProto.reduce) ?
function(a, init, f, o) {
// ES5 Array.reduce doesn't support a thisObject, so we need to
- // implement it manually
+ // implement it manually.
return a.reduce(function(init, item, i, a) {
return f.call(o, init, item, i, a);
}, init);
View
21 src/yui/HISTORY.md
@@ -4,13 +4,24 @@ YUI Core Change History
3.5.0
-----
+* YUI now runs natively on Node.js without a shim. See README.nodejs.md for
+ details.
+
+* YUI now detects non-native ES5 shims added to native objects by other
+ libraries and falls back to its own internal shims rather than relying on the
+ potentially broken code from the other library.
+
* `Y.Object.isEmpty()` now casts the given value to an object if it isn't one
already, which prevents exceptions when it's given a non-object.
-* 2530970 Should we provide a YUI.applyConfig(), to avoid clobbering of YUI_config in 'mashup' use cases
-* 2531247 namespace function behaves wrong with multiple arguments
-* 2531512 'debug' parameter missing from the YUI Config object documentation; the Config object documentation ...
-* YUI is now native on Node.js, no shim needed to run. See README.nodejs.md
-
+
+* Added static YUI.applyConfig to apply config settings to YUI.GlobalConfig in
+ parts instead of in whole. [Ticket #2530970]
+
+* Fixed issue #2531247: Namespace function behaves wrong with multiple
+ arguments.
+
+* Fixed issue #2531512: 'debug' parameter missing from the YUI Config object
+ documentation.
3.4.1
View
6 src/yui/js/yui-array.js
@@ -107,7 +107,7 @@ the native ES5 `Array.forEach()` method if available.
@return {YUI} The YUI instance.
@static
**/
-YArray.each = YArray.forEach = Native.forEach ? function (array, fn, thisObj) {
+YArray.each = YArray.forEach = Lang._isNative(Native.forEach) ? function (array, fn, thisObj) {
Native.forEach.call(array || [], fn, thisObj || Y);
return Y;
} : function (array, fn, thisObj) {
@@ -171,7 +171,7 @@ This method wraps the native ES5 `Array.indexOf()` method if available.
found.
@static
**/
-YArray.indexOf = Native.indexOf ? function (array, value) {
+YArray.indexOf = Lang._isNative(Native.indexOf) ? function (array, value) {
// TODO: support fromIndex
return Native.indexOf.call(array, value);
} : function (array, value) {
@@ -223,7 +223,7 @@ value from the function will stop the processing of remaining items.
items in the array; `false` otherwise.
@static
**/
-YArray.some = Native.some ? function (array, fn, thisObj) {
+YArray.some = Lang._isNative(Native.some) ? function (array, fn, thisObj) {
return Native.some.call(array, fn, thisObj);
} : function (array, fn, thisObj) {
for (var i = 0, len = array.length; i < len; ++i) {
View
43 src/yui/js/yui-lang.js
@@ -30,15 +30,40 @@ TYPES = {
'[object Error]' : 'error'
},
-SUBREGEX = /\{\s*([^|}]+?)\s*(?:\|([^}]*))?\s*\}/g,
-TRIMREGEX = /^\s+|\s+$/g,
+SUBREGEX = /\{\s*([^|}]+?)\s*(?:\|([^}]*))?\s*\}/g,
+TRIMREGEX = /^\s+|\s+$/g,
+NATIVE_FN_REGEX = /\{\s*\[(?:native code|function)\]\s*\}/i;
-// If either MooTools or Prototype is on the page, then there's a chance that we
-// can't trust "native" language features to actually be native. When this is
-// the case, we take the safe route and fall back to our own non-native
-// implementation.
-win = Y.config.win,
-unsafeNatives = win && !!(win.MooTools || win.Prototype);
+// -- Protected Methods --------------------------------------------------------
+
+/**
+Returns _true_ if the given function appears to be implemented in native code,
+_false_ otherwise. This isn't guaranteed to be 100% accurate and won't work for
+anything other than functions, but it can be useful for determining whether
+a function like `Array.prototype.forEach` is native or a JS shim provided by
+another library.
+
+There's a great article by @kangax discussing the flaws with this technique:
+<http://perfectionkills.com/detecting-built-in-host-methods/>
+
+While his points are valid, it's still possible to benefit from this function
+as long as it's used carefully and sparingly, and in such a way that false
+negatives have minimal consequences. It's used internally to avoid using
+potentially broken non-native ES5 shims that have been added to the page by
+other libraries.
+
+@method _isNative
+@param {Function} fn Function to test.
+@return {Boolean} _true_ if _fn_ appears to be native, _false_ otherwise.
+@static
+@protected
+@since 3.5.0
+**/
+L._isNative = function (fn) {
+ return !!(fn && NATIVE_FN_REGEX.test(fn));
@jdalton
jdalton added a note Apr 6, 2012

Possibly a more robust isNative method here.

@rgrove
rgrove added a note Apr 9, 2012

Thanks for the link (and sorry for the delay responding -- was on vacation). Do you have an example of a case where this test fails and your more robust version succeeds? So far I haven't seen a failure in any common browser, and I'd prefer not to increase the complexity of the test unless there's a compelling reason.

@jdalton
jdalton added a note Apr 13, 2012

Thanks for the link (and sorry for the delay responding -- was on vacation). Do you have an example of a case where this test fails and your more robust version succeeds? So far I haven't seen a failure in any common browser, and I'd prefer not to increase the complexity of the test unless there's a compelling reason.

The one I linked to covers this case too [ecmascript code] which may have occurred in some version of Opera/Safari (@dperini would have more details). The advantage is it doesn't assume a specific pattern and uses existing native methods to detect the pattern in use so if another pattern happens to show up it shouldn't require an update. That said the snippet I linked to could use more comments, cleanup, and moar tests ;D

@jdalton
jdalton added a note May 6, 2012

@rgrove off hand do you know which browser returns [function] ?

@rgrove
rgrove added a note May 6, 2012

Ancient versions of Safari. <= 2 if I remember correctly. I don't think anything else does.

@Krinkle
Krinkle added a note May 7, 2012

Another one I often use:

rNativeFunc = new RegExp('^' + K.escapeRE(({}.valueOf + '')).replace(/valueOf/g, '(.+)') + '$');

Adapt for YUI maybe?:

NATIVE_FN_REGEX = new RegExp('^' + Y.Escape.regex(({}.valueOf + '')).replace(/valueOf/g, '(.+)') + '$');

At least it doesn't hard code the internal code used to indicate a native function. It is not as elaborate as the one @jdalton linked though. This does cover more cases and is more future proof - than the one @rgrove committed, while still keeping isNative simple.

In Chrome, for example, the resulting regex looks like this:

/^function (.+)\(\) \{ \[native code\] \}$/

Usage:

NATIVE_FN_REGEX.test(fn);
@rgrove
rgrove added a note May 7, 2012

Not a bad idea.

@jdalton
jdalton added a note Jun 4, 2012

I ended up using @Krinkle's suggestion with a tweak for Java environments (Ringo/Rhino/Narwhal):
https://github.com/bestiejs/lodash/blob/231a4e487556053952fba6ec140d589372f4895c/lodash.js#L49-52

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+};
+
+// -- Public Methods -----------------------------------------------------------
/**
* Determines whether or not the provided item is an array.
@@ -52,7 +77,7 @@ unsafeNatives = win && !!(win.MooTools || win.Prototype);
* @return {boolean} true if o is an array.
* @static
*/
-L.isArray = (!unsafeNatives && Array.isArray) || function (o) {
+L.isArray = L._isNative(Array.isArray) ? Array.isArray : function (o) {
return L.type(o) === 'array';
};
View
20 src/yui/js/yui-object.js
@@ -12,16 +12,10 @@
* @class Object
*/
-var hasOwn = Object.prototype.hasOwnProperty,
+var Lang = Y.Lang,
+ hasOwn = Object.prototype.hasOwnProperty,
-// If either MooTools or Prototype is on the page, then there's a chance that we
-// can't trust "native" language features to actually be native. When this is
-// the case, we take the safe route and fall back to our own non-native
-// implementations.
-win = Y.config.win,
-unsafeNatives = win && !!(win.MooTools || win.Prototype),
-
-UNDEFINED, // <-- Note the comma. We're still declaring vars.
+ UNDEFINED, // <-- Note the comma. We're still declaring vars.
/**
* Returns a new object that uses _obj_ as its prototype. This method wraps the
@@ -34,7 +28,7 @@ UNDEFINED, // <-- Note the comma. We're still declaring vars.
* @return {Object} New object using _obj_ as its prototype.
* @static
*/
-O = Y.Object = (!unsafeNatives && Object.create) ? function (obj) {
+O = Y.Object = Lang._isNative(Object.create) ? function (obj) {
// We currently wrap the native Object.create instead of simply aliasing it
// to ensure consistency with our fallback shim, which currently doesn't
// support Object.create()'s second argument (properties). Once we have a
@@ -145,8 +139,8 @@ O.hasKey = owns;
* @return {String[]} Array of keys.
* @static
*/
-O.keys = (!unsafeNatives && Object.keys) || function (obj) {
- if (!Y.Lang.isObject(obj)) {
+O.keys = Lang._isNative(Object.keys) ? Object.keys : function (obj) {
+ if (!Lang.isObject(obj)) {
throw new TypeError('Object.keys called on a non-object');
}
@@ -320,7 +314,7 @@ O.some = function (obj, fn, thisObj, proto) {
* if an empty path is provided.
*/
O.getValue = function(o, path) {
- if (!Y.Lang.isObject(o)) {
+ if (!Lang.isObject(o)) {
return UNDEFINED;
}
View
21 src/yui/tests/lang-test.js
@@ -3,13 +3,32 @@ YUI.add('lang-test', function (Y) {
var Assert = Y.Assert,
Lang = Y.Lang,
- win = Y.config.win,
+ doc = Y.config.doc,
suite = new Y.Test.Suite('Y.Lang');
suite.add(new Y.Test.Case({
name: 'Lang tests',
+ '_isNative() should return true for native functions': function () {
+ Assert.isTrue(Lang._isNative(Object.prototype.toString), 'Object.prototype.toString is native');
+ Assert.isTrue(Lang._isNative(Array.prototype.concat), 'Array.prototype.concat is native');
+ Assert.isTrue(Lang._isNative(String.prototype.replace), 'String.prototype.replace is native');
+
+ if (doc) { // may not exist in Node.js
+ Assert.isTrue(Lang._isNative(doc.getElementById), 'document.getElementById is native');
+ Assert.isTrue(Lang._isNative(doc.getElementsByTagName('body')[0].cloneNode), 'DOM cloneNode() is native');
+ }
+ },
+
+ '_isNative() should return false for non-native functions': function () {
+ Assert.isFalse(Lang._isNative(Lang._isNative), 'Lang._isNative is not native');
+ Assert.isFalse(Lang._isNative(YUI), 'YUI is not native');
+ Assert.isFalse(Lang._isNative(function () {}, 'An anonymous function is not native'));
+ Assert.isFalse(Lang._isNative(function () { '[native code]' }, 'Tricky non-native function is not native'));
+ Assert.isFalse(Lang._isNative(function () { return '[native code]'; }, 'Anothre tricky non-native function is not native'));
+ },
+
test_is_array: function() {
Assert.isTrue(Lang.isArray([1, 2]), "Array literals are arrays");
Assert.isFalse(Lang.isArray({"one": "two"}), "Object literals are not arrays");

0 comments on commit 3e4a3c3

Please sign in to comment.
Something went wrong with that request. Please try again.