Add Ability to Easily Extend string.js by jeffgrann · Pull Request #57 · jprichardson/string.js (original) (raw)
Making string.js More Extensible
I love string.js! However, I'd like to have the ability to easily extend it in a safe, consistent and simple manner.
Why?
Simply stated, I have requirements that string.js does not meet on its own. I'm sure that others do too. Now someone in my position could simply modify string.js and submit a pull request. But what if some of the changes really don't belong in a generic library like string.js? What if they're too specific to a particular application domain? The author, as they should, would nix those changes.
So extending string.js, just like the author has extended the native Javascript String object, is the way to go.
Why Not Just Modify the String.js Prototype?
What if you're working on a large project with several developers or using other libraries which depend on string.js and you modify string.js? Now you're in the same boat as you would be if you modified the String Object prototype. Other modules may use string.js and expect certain behavior that you have modified. Bugs galore!
For an example, one of my requirements is case insensitivity with the ability to use case sensitivity on demand. If I modify the string.js replaceAll method to make it default to case insensitive matches, other code that expects string.js to be case sensitive will break.
How?
While attempting to create such a string.js extension, I found that string.js needed to be modified in four simple ways. The good news is, none of the changes affect the module's interface. Everything works just like before. The changes just make it more generic and extensible.
1. Add an initialze
function containing the current S
constructor's code.
The S
constructor and the new setValue
method (see below) both call this new function.
2. Add a setValue
method.
This method allows the constructor in a module which extends string.js to set the string value just like the S
constructor without knowing how to set the necessary values (s
and orig
) itself.
The constructor in this other module would look something like this:
ExtendedString.prototype = S('');
ExtendedString.prototype.constructor = ExtendedString;
function ExtendedString (value) { this.setValue(value); }
3. Change returned values from methods.
Instead of returning a new S object, methods must return a new object using the given object's constructor. For string.js objects, the constructor will be S. For extended objects, it will be the object's constructor (ExtendedString
in the example above). This way, string.js methods will always return the same kind of object that they were given.
Doing this prevents each extended module from having to jump through hoops to pull in the string.js methods and force them to return its own kind of object. This is what string.js has to do to get the native String object methods to return string.js objects (in the Attach Native JavaScript String Properties section).
So each method that returns a new string, now does this:
return new this.constructor(s);
Instead of this:
4. Set the constructor to S
after setting the prototype.
When setting the prototype of S to the object containing the methods, the constructor is obliterated. So string.js objects are instanceof Object
but they're not instanceof S
when they should be.
Extension Example
The following code shows a string.js extension module which creates and manipulates ExtendedStrings
. It modifies the contains
string.js method to default to case-insensitive searches and provides callers with a way to force the search to be case sensitive.
var CASE_OPTIONS; var parentPrototype; var stringJSObject;
//------------------------------------------------------------------------------------- // caseOptions //------------------------------------------------------------------------------------- CASE_OPTIONS = Object.freeze({ CASE_INSENSITIVE : 'CASE_INSENSITIVE', CASE_SENSITIVE : 'CASE_SENSITIVE' });
//------------------------------------------------------------------------------------- // ExtendedStrings constructor //------------------------------------------------------------------------------------- stringJSObject = S('');
parentPrototype = Object.getPrototypeOf(stringJSObject);
ExtendedStrings.prototype = stringJSObject;
ExtendedStrings.prototype.constructor = ExtendedStrings;
function ExtendedStrings (value) { this.setValue(value); }
//------------------------------------------------------------------------------------- // extendedStringMaker //------------------------------------------------------------------------------------- function extendedStringMaker (value) { if (value instanceof ExtendedStrings) { return value; }
return new ExtendedStrings(value);
};
//------------------------------------------------------------------------------------- // contains //------------------------------------------------------------------------------------- ExtendedStrings.prototype.contains = function contains (value, caseOption) { if (caseOption === CASE_OPTIONS.CASE_SENSITIVE) { return parentPrototype.contains.call(this, value); } else { return parentPrototype.contains.call(this.toUpperCase(), value.toUpperCase()); } };
//------------------------------------------------------------------------------------- // Set this module's public interface. //------------------------------------------------------------------------------------- extendedStringMaker.CASE_OPTIONS = CASE_OPTIONS;
return extendedStringMaker;
Example usage:
extendedStringMaker('This is a test').contains('this'); // true