Object in NodeList prototype - javascript

The prototype of some native javascript objects can be extended to include new functions (such as NodeList.prototype.forEach or NodeList.prototype.addEventListener) which I use to allow array and element-like interaction with the NodeList. So far so good, but how should I add an object to the prototype which in turn has it's own functions (to allow NodeListVar.classList.remove("class")). I have been able to make NodeListVar.classList().remove("class"), by doing the following
NodeList.prototype.classList = function(){
var _this = this;
return {
remove: function(class){
_this.forEach(function(){
this.classList.remove(class);
});
}
}
};
However I would greatly prefer the syntax to be the same as a normal element, thus more like:
NodeList.prototype.classList = {
remove: function(class){
//where *this* would be the nodeList and *not* the DOMWindow
this.forEach(function(){
this.classList.remove(class);
});
}
};
It probably isn't hard even, but I have searched google endlessly and looked through countless questions already and can't find anything usefull.

First to read: What’s wrong with extending the DOM.
You can't set objects on a prototype. All called functions will be executed in context of that static prototype object, not the NodeList itself. The object on the prototype has no reference to the current NodeList.
On normal Elements, every element has its own classList attribute, a DOMTokenList bound the element. You will need to do the same: Give every NodeList its own classLists instance. As you can't do that in the unavailable constructor, you will have to use a getter, as already demonstrated.
I don't think you should try to allow the same syntax on NodeLists as on Elements, because they are very different. But if you want to get rid of these brackets, you can install a native getter function.
function ClassLists(nl) {
... // nl references the NodeList we're bound to
}
ClassLists.prototype = ...;
Object.defineProperty(NodeList.prototype, "classLists", {
get: function() {
return this.classLists = new ClassLists(this);
}
});

Just to complement #Bergi's answer, here is a cross-browser solution:
function ClassList() {
this.nodeList;
}
ClassList.prototype = {
constructor: ClassList,
remove: function() {
console.log( this );
}
};
NodeList.prototype.classList = new ClassList();
nl.classList.remove(); // ClassList object with the `nodeList` property.
It can't be used with the same API since it's not using defineProperty, but it's cross-browser. defineProperty is only available in IE9, and is not shimmable in IE7.

Related

Why doesn't merging a prototype to another object via Object.assign() work?

So I have this class definition:
class Field {
constructor(canvas) {
const CANVAS = document.querySelector(canvas);
const CONTEXT = CANVAS.getContext("2d");
return Object.assign(CONTEXT, Field.prototype); // *
}
prototypeMethodName() {
return "something";
}
}
console.log(new Field("canvas"));
<canvas></canvas>
The main idea here is that when I invoke new Field() I get not the instance of Field, but the object consisting of two other objects: CONTEXT and Field.prototype. The CONTEXT is the instance of the CanvasRenderingContext2D, and, basically, I just want to augument it with other methods (in this example it's just one method prototypeMethodName()).
But in this case I get a bare CONTEXT object that doesn't contain any of "my" properties.
I also noticed that the assign method doesn't work correctly only when I'm trying to merge a prototype of something to the tatget object. So the thing here is not about the CONTEXT object, but is about the prototype.
Why doesn't my code work? What would I do to make it work then?
I just want to augument it with other methods
Don't do that. Use composition instead.
But I get a bare CONTEXT object that doesn't contain any of "my" properties.
I also noticed that the assign method doesn't work correctly only when I'm trying to merge a prototype of something to the target object.
Yes. The prototype objects of classes are not containing any enumerable methods, so Object.assign doesn't find them. Have a look at ES6 Iterate over class methods.
try this
merge(context, field) {
for(let prop in Field.prototype) {
context.prototype[prop] = Field.prototype[prop];
}
return context;
}

array like object using array methods [duplicate]

I was looking at some snippets of code, and I found multiple elements calling a function over a node list with a forEach applied to an empty array.
For example I have something like:
[].forEach.call( document.querySelectorAll('a'), function(el) {
// whatever with the current node
});
but I can't understand how it works. Can anyone explain me the behaviour of the empty array in front of the forEach and how the call works?
[] is an array.
This array isn't used at all.
It's being put on the page, because using an array gives you access to array prototypes, like .forEach.
This is just faster than typing Array.prototype.forEach.call(...);
Next, forEach is a function which takes a function as an input...
[1,2,3].forEach(function (num) { console.log(num); });
...and for each element in this (where this is array-like, in that it has a length and you can access its parts like this[1]) it will pass three things:
the element in the array
the index of the element (third element would pass 2)
a reference to the array
Lastly, .call is a prototype which functions have (it's a function which gets called on other functions).
.call will take its first argument and replace this inside of the regular function with whatever you passed call, as the first argument (undefined or null will use window in everyday JS, or will be whatever you passed, if in "strict-mode"). The rest of the arguments will be passed to the original function.
[1, 2, 3].forEach.call(["a", "b", "c"], function (item, i, arr) {
console.log(i + ": " + item);
});
// 0: "a"
// 1: "b"
// 2: "c"
Therefore, you're creating a quick way to call the forEach function, and you're changing this from the empty array to a list of all <a> tags, and for each <a> in-order, you are calling the function provided.
EDIT
Logical Conclusion / Cleanup
Below, there's a link to an article suggesting that we scrap attempts at functional programming, and stick to manual, inline looping, every time, because this solution is hack-ish and unsightly.
I'd say that while .forEach is less helpful than its counterparts, .map(transformer), .filter(predicate), .reduce(combiner, initialValue), it still serves purposes when all you really want to do is modify the outside world (not the array), n-times, while having access to either arr[i] or i.
So how do we deal with the disparity, as Motto is clearly a talented and knowledgeable guy, and I would like to imagine that I know what I'm doing/where I'm going (now and then... ...other times it's head-first learning)?
The answer is actually quite simple, and something Uncle Bob and Sir Crockford would both facepalm, due to the oversight:
clean it up.
function toArray (arrLike) { // or asArray(), or array(), or *whatever*
return [].slice.call(arrLike);
}
var checked = toArray(checkboxes).filter(isChecked);
checked.forEach(listValues);
Now, if you're questioning whether you need to do this, yourself, the answer may well be no...
This exact thing is done by... ...every(?) library with higher-order features these days.
If you're using lodash or underscore or even jQuery, they're all going to have a way of taking a set of elements, and performing an action n-times.
If you aren't using such a thing, then by all means, write your own.
lib.array = (arrLike, start, end) => [].slice.call(arrLike, start, end);
lib.extend = function (subject) {
var others = lib.array(arguments, 1);
return others.reduce(appendKeys, subject);
};
Update for ES6(ES2015) and Beyond
Not only is a slice( )/array( )/etc helper method going to make life easier for people who want to use lists just like they use arrays (as they should), but for the people who have the luxury of operating in ES6+ browsers of the relatively-near future, or of "transpiling" in Babel today, you have language features built in, which make this type of thing unnecessary.
function countArgs (...allArgs) {
return allArgs.length;
}
function logArgs (...allArgs) {
return allArgs.forEach(arg => console.log(arg));
}
function extend (subject, ...others) { /* return ... */ }
var nodeArray = [ ...nodeList1, ...nodeList2 ];
Super-clean, and very useful.
Look up the Rest and Spread operators; try them out at the BabelJS site; if your tech stack is in order, use them in production with Babel and a build step.
There's no good reason not to be able to use the transform from non-array into array... ...just don't make a mess of your code doing nothing but pasting that same ugly line, everywhere.
The querySelectorAll method returns a NodeList, which is similar to an array, but it's not quite an array. Therefore, it doesn't have a forEach method (which array objects inherit via Array.prototype).
Since a NodeList is similar to an array, array methods will actually work on it, so by using [].forEach.call you are invoking the Array.prototype.forEach method in the context of the NodeList, as if you had been able to simply do yourNodeList.forEach(/*...*/).
Note that the empty array literal is just a shortcut to the expanded version, which you will probably see quite often too:
Array.prototype.forEach.call(/*...*/);
The other answers have explained this code very well, so I'll just add a suggestion.
This is a good example of code that should be refactored for simplicity and clarity. Instead of using [].forEach.call() or Array.prototype.forEach.call() every time you do this, make a simple function out of it:
function forEach( list, callback ) {
Array.prototype.forEach.call( list, callback );
}
Now you can call this function instead of the more complicated and obscure code:
forEach( document.querySelectorAll('a'), function( el ) {
// whatever with the current node
});
It can be better written using
Array.prototype.forEach.call( document.querySelectorAll('a'), function(el) {
});
What is does is document.querySelectorAll('a') returns an object similar to an array, but it does not inherit from the Array type.
So we calls the forEach method from the Array.prototype object with the context as the value returned by document.querySelectorAll('a')
[].forEach.call( document.querySelectorAll('a'), function(el) {
// whatever with the current node
});
It is basically the same as:
var arr = document.querySelectorAll('a');
arr.forEach(function(el) {
// whatever with the current node
});
Want to update on this old question:
The reason to use [].foreach.call() to loop through elements in the modern browsers is mostly over. We can use document.querySelectorAll("a").foreach() directly.
NodeList objects are collections of nodes, usually returned by
properties such as Node.childNodes and methods such as
document.querySelectorAll().
Although NodeList is not an Array, it is possible to iterate over it
with forEach(). It can also be converted to a real Array using
Array.from().
However, some older browsers have not implemented NodeList.forEach()
nor Array.from(). This can be circumvented by using
Array.prototype.forEach() — see this document's Example.
Lots of good info on this page (see answer+answer+comment), but I recently had the same question as the OP, and it took some digging to get the whole picture. So, here's a short version:
The goal is to use Array methods on an array-like NodeList that doesn't have those methods itself.
An older pattern co-opted Array's methods via Function.call(), and used an array literal ([]) rather than than Array.prototype because it was shorter to type:
[].forEach.call(document.querySelectorAll('a'), a => {})
A newer pattern (post ECMAScript 2015) is to use Array.from():
Array.from(document.querySelectorAll('a')).forEach(a => {})
An empty array has a property forEach in its prototype which is a Function object. (The empty array is just an easy way to obtain a reference to the forEach function that all Array objects have.) Function objects, in turn, have a call property which is also a function. When you invoke a Function's call function, it runs the function with the given arguments. The first argument becomes this in the called function.
You can find documentation for the call function here. Documentation for forEach is here.
Just add one line:
NodeList.prototype.forEach = HTMLCollection.prototype.forEach = Array.prototype.forEach;
And voila!
document.querySelectorAll('a').forEach(function(el) {
// whatever with the current node
});
Enjoy :—)
Warning: NodeList is a global class. Don't use this recomendation if you writing public library. However it's very convenient way for increasing self-efficacy when you work on website or node.js app.
Just a quick and dirty solution I always end up using. I wouldn't touch prototypes, just as good practice. Of course, there are a lot of ways to make this better, but you get the idea.
const forEach = (array, callback) => {
if (!array || !array.length || !callback) return
for (var i = 0; i < array.length; i++) {
callback(array[i], i);
}
}
forEach(document.querySelectorAll('.a-class'), (item, index) => {
console.log(`Item: ${item}, index: ${index}`);
});
[] always returns a new array, it is equivalent to new Array() but is guaranteed to return an array because Array could be overwritten by the user whereas [] can not. So this is a safe way to get the prototype of Array, then as described, call is used to execute the function on the arraylike nodelist (this).
Calls a function with a given this value and arguments provided
individually. mdn
Norguard explained WHAT [].forEach.call() does and James Allardice WHY we do it: because querySelectorAll returns a NodeList that doesn't have a forEach method...
Unless you have modern browser like Chrome 51+, Firefox 50+, Opera 38, Safari 10.
If not you can add a Polyfill:
if (window.NodeList && !NodeList.prototype.forEach) {
NodeList.prototype.forEach = function (callback, thisArg) {
thisArg = thisArg || window;
for (var i = 0; i < this.length; i++) {
callback.call(thisArg, this[i], i, this);
}
};
}
let's say you have : const myList= document.querySelectorAll("p");
This will return an list/array of all in your HTML.
Now Array.prototype.forEach.call(myList, myCallback)
is equivalent to [].forEach.call(myList, myCallback)
where 'myCallback' is a callback function.
You are basically running the callback function on each element of myList.
Hope this helped you!
I don't know if there is any restriction, but it works.
I turned the nodeList into an iterator object using the spread operator and mapped it:
let _btns = document.querySelectorAll('.btn');
[..._btns].map(function(elem, i) {
elem.addEventListener('click', function (e) {
console.log(elem.textContent);
})
})
.btn {
padding: 5px;
color:#fff;
background-color: darkred;
text-align:center;
color: white;
}
<button class="btn">button 1</button>
<button class="btn">button 2</button>

Issue in Creating JavaScript Selectors

I have an Issue Here in creating my Selectors i do need to have it like exactly jquery do
in jquery in each Set Chain it returns the Array Holding the Elements results from the selector, while i achieve this phase from scripting my selectors
var Ary = [];
var Selector = function(selectorFormant){
// Some Script using querySelectorAll and pushing Elements to Ary
return Ary;
}
Ary.getByTypes=function(typesString){
//Some Script here take the elements inside the Ary and get the elements inside them with the specific types typesString and repush new elements in the Ary
return Ary;
}
Selector.prototype = Ary;
Selector.prototype.constructor = Selector;
//this script is responsible for inheritance from Ary to Selector Class
my issue here that the Developer can use the selector class in two ways
1- Selector.getByTypes('types')
or
2- Selector('selector format like jquery').getByTypes('types')
in 2 i dont have to instantiate a object to apply the inheritance i preformed becuase the method Selector return the Ary which have the Function getByTypes
but in 1 i have to instantiate a object from Selector to apply the inheritance to have the Ary members for me when i dont need the developer write the new keyword
2 I dont need that- new Selector('selector format').getByTypes('types');
any one can help please :)
It seems what you actually want is:
function Selector(sel) {
// check if the constructor was used with "new". If not, correct that:
if (! (this instanceof Selector)) return new Selector(sel);
// Notice that "this" is an empty object, inheriting from the prototype.
var els = document.querySelectorAll(sel);
for (var i=0; i<els.length; i++)
this.push(els[i]); // use the inherited method "push"
// even though this is not an actual Array instance
// Don't return anything, there's an implizit "return this;" as we used "new"
}
// Let the prototype object inherit from Array.prototype
Selector.prototype = Object.create(Array.prototype, {
constructor: {value:Selector, configurable:true} // and (re)sest constructor
});
// define "getByTypes" method for all instances:
Selector.prototype.getByTypes = function(typ) {
// Do something with "this"
…
// and for chainability return a Selector instance, either a new one or just
return this;
};
If you really need Selector.getByTypes() - something like a "class method" - you could add a (completely unrelated) function on that property, doing what you want:
Selector.getByTypes = function(typ) {
return new Selector("*").getByTypes(typ); // or anything else
};
…however, just writing Selector("button") seems easier to me if getByTypes is doing a search in the selected elements.
How about the querySelectorAll method (although only supported by newer browsers)?
https://developer.mozilla.org/en-US/docs/DOM/document.querySelectorAll

How to add Nodelist/HtmlColection new method by prototype?

Both typeof document.querySelectorAll('a') and document.getElementsByTagName('a') is Object, so I why I can't add a method in this way:
Object.prototype.method = function(){ alert(this); }
But, this work:
a = document.getElementsByTagName('a');
a.__proto__.__proto__.method = function(){ alert(this); }
a.method();//object HTMLCollection
I thought a.__proto__ == a.constructor.prototype, does it?
Using Object.prototype is possible. However I do not recommend messing up with it.
instance_of_my_object = document.getElementsByTagName('a'); is a single instance of your Object. In this case instance_of_my_object is a NodeList.
instance_of_my_object.method = ... attaches your method or variable to this single instance of NodeList.
If you want to attach some method to all the instances of a class then you can use prototype (like this : className.prototype.myMethod = function () ...).
I found this very useful in my case:
NodeList.prototype.toArray = function() {
return Array.prototype.slice.call(this);
};
This both shows how to add a method to NodeList in this case, and how to turn them to old simple arrays and have the options of an array like concat, foreach, and so on.
You can do the same for HTMLCollection, too. Have fun :)

Copy and extend global objects in javascript

is there a way to copy a global object (Array,String...) and then extend the prototype of the copy without affecting the original one? I've tried with this:
var copy=Array;
copy.prototype.test=2;
But if i check Array.prototype.test it's 2 because the Array object is passed by reference. I want to know if there's a way to make the "copy" variable behave like an array but that can be extended without affecting the original Array object.
Good question. I have a feeling you might have to write a wrapper class for this. What you're essentially doing with copy.prototype.test=2 is setting a class prototype which will (of course) be visible for all instances of that class.
I think the reason the example in http://dean.edwards.name/weblog/2006/11/hooray/ doesn't work is because it's an anonymous function. So, instead of the following:
// create the constructor
var Array2 = function() {
// initialise the array
};
// inherit from Array
Array2.prototype = new Array;
// add some sugar
Array2.prototype.each = function(iterator) {
// iterate
};
you would want something like this:
function Array2() {
}
Array2.prototype = new Array();
From my own testing, the length property is maintained in IE with this inheritance. Also, anything added to MyArray.prototype does not appear to be added to Array.prototype. Hope this helps.
Instead of extending the prototype, why don't you simply extend the copy variable. For example, adding a function
copy.newFunction = function(pParam1) {
alert(pParam1);
};

Categories