When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.

If a bird walks like a duck, swims like a duck, quacks like a duck, it can be called a duck. — James Whitcomb Riley, 1849-1916

disclaimer

This article is for entertainment purposes only, and some of the code is quite confusing and not recommended for use in a production environment

What is a duck type

Search engine search, you can find the following text:

In programming, Duck typing (English: Duck typing) is an object inference style for dynamic typing and some static languages. In the duck type, the focus is not on the type of the object itself, but on how it is used. The interpreter/compiler for languages that support duck types will infer the type of the object at Parse or compile time.

Simply put, an object’s object type can be inferred by checking whether it has certain properties or methods. There are quite a few duck types in JavaScript, here are some examples:

ArrayLike class array objects

Typing objects such as String, arguments, etc., have length attributes and use numbers as access subscripts like arrays, but they are not arrays themselves.

interfact ArrayLike<T> {
	[key: string]? : T,readonly length: number
}
Copy the code

Judgment method

const isArrayLike = array= > array && typeof array.length === 'number'
Copy the code

Using the JavaScript duck type feature, we can call the array prototype method with call and apply, so that the array prototype method can handle these data:

const arrLike = { 
	'0': 1.'1': 2.'2': 3.length: 3 
}

// [].slice == array.prototype
[].slice.call(arrLike) / / [1, 2, 3]

[].map.call(arrLike, item= > item + 1) / / [2, 3, 4]

[].filter.call(arrLike, item= >item ! = =2) / / [1, 3]

[].reduce.call(arrLike, (prev, curr) = > prev + curr, 0) / / 6

[].map.call('123'.Number) / / [1, 2, 3]
Copy the code

Iterable Iterable object

If an object or its prototype has the symbol. iterator method:

const iterable = {
	*[Symbol.iterator] () {
		yield 1;
		yield 2;
		yield 3; }}; [...iterable]/ / [1, 2, 3]
Copy the code

This function is called the iterator function. The object extends the operator by calling the iterator function… Expand or for… Of iteration, we call this object implementing the iteration protocol, this object is an iterable.

In ES6, the Array, String, arguments, Set, Map, FormData, and other constructors all have their own symbol. iterator functions on their prototypes. The above arrLike can be treated as an array, but it cannot be iterated because it does not implement the iteration protocol. We need to add an iterator function to the arrLike:

arrLike[Symbol.iterator] = function* () {
	let i = 0;
	while (i < this.length) {
		yield this[i];
		i++;
	}
}

[...arrLike] / / [1, 2, 3]
Copy the code

Of course, you can use a normal function to return an Iterator that matches the Iterator type described below, but the code is too tedious to show, please move to the iteration protocol

Iterable typing says:

interface Iterable<T> {
    [Symbol.iterator](): Iterator<T>;
}
Copy the code

Where the Iterator needs to provide the next, return, and throw methods that return the same value as when the generator function is called:

interfaceIterator<T> { next(value? :any): IteratorResult<T>;
    return? (value? :any): IteratorResult<T>;
    throw? (e? :any): IteratorResult<T>;
}
Copy the code

Determines whether it is an iterable

const iterable = data= > 
	typeof Symbol! = ='undefined' 
	&& typeof data[Symbol.iterator] === 'function'
Copy the code

Thenable object

When we call new Promise(()=>{}), we return an object containing methods like THEN, catch, finally, and so on. Methods with then functions are referred to as Thenable objects, or PromiseLike objects. What is the meaning of this object? Refer to the following code:

const thenable = {
	then(res) {
		setTimeout(res, 1000)}}/ / 1
Promise.resolve()
	.then(() = >thenable)
	.then(() = >console.log('A second goes by'));

/ / 2
!async function() {
	const sleep = () = > thenable

	await sleep();
	console.log('A second goes by'); } ();Copy the code

Both statements execute as expected (wait one second to print), proving that Promise can determine if an object needs to wait for its resolved by simply determining if it has a THEN function. Is it very simple and crude?

Thenable tying:

interface Thenable<T> {
    then<T, N = never> (
    	resolve: (value: T) = > T | Thenable<T> | void.reject: (reason: any) = > N | Thenable<N> | void
	): Thenable<T | N>
}
Copy the code

Judgment method:

const thenable = fn= > fn.then && typeof fn.then === 'function'
Copy the code

Entries

For an object {a: 1, b: 2, c: 3}, use [key, value] as a two-dimensional array of elements:

[['a'.1],
	['b'.2],
	['c'.3]]Copy the code

Called Entries, Entries are iterable, need to implement iterable protocols, and cannot be primitive type data (such as strings)

interface Entries<K,V> {
	[key: number]: [K, V],
	[Symbol.iterator](): Iterator<T>;
}
Copy the code

Judgment method:

const isEntries = data= > {
	if (typeof data[Symbol.iterator] ! = ='function') {
    	return false;
	}
    return Object.values(data).every(d= > Array.isArray(d) && d.length >= 2)}Copy the code

Object. The entries

Calling Object.entries converts objects with key-value pairs into entries

const entry = Object.entries({ a: 1.b: 2.c: 3 }) // [['a', 1], ['b', 2], ['c', 3]]

const map = new Map()
map.set('a'.1)
map.set('b'.2)
map.set('c'.3)

Object.entries(map) // [['a', 1], ['b', 2], ['c', 3]]

const fd = new FormData()
fd.set('a'.1)
fd.set('b'.2)
fd.set('c'.3)

Object.entries(fd) // [['a', 1], ['b', 2], ['c', 3]]

Object.entries('abc') // [['0','a'],['1','b'],['2','c']]
Copy the code

Among them, array, Map, Set, FormData and other reference types of prototype on its own entries method, called after the return of a generator object, you can use the extended operator… A:

const arrIterator = ['a'.'b'.'c'].entries();
[...arrIterator] // [['0','a'],['1','b'],['2','c']]

const setIterator = new Set(['a'.'b'.'c']).entries();

[...setIterator] // [['a', 'a'], ['b', 'b'], ['c', 'c']]

const map = new Map(a); map.set('a'.1)
map.set('b'.2)
map.set('c'.3)

const mapIterator = map.entries();
[...mapIterator] // [['a', 1], ['b', 2], ['c', 3]]

const fd = new FormData();
fd.set('a'.1)
fd.set('b'.2)
fd.set('c'.3)

const fdIterator = fd.entries(fd);
[...fdIterator] // [['a', 1], ['b', 2], ['c', 3]]
Copy the code

Note that since this is a generator object, once the iteration is complete, calling the next method or expanding does not spit out any value.

Object. FromEntries and Map constructors

Object.fromEntries is the syntax defined by ECMAScript 2019 (not compatible with older browsers) and, in contrast to Object.entries, changes the entries Object to Object

Object.fromEntries( [['a', 1], ['b', 2], ['c', 3]] ) // { a:1, b:2, c: 3 }
Copy the code

For FormData or Map objects, this is implicitly converted to Entries

const fd = new FormData()
fd.set('a'.1)
fd.set('b'.2)
fd.set('c'.3)

Object.fromEntries(fd) // [['a', 1], ['b', 2], ['c', 3]]

const map = new Map()
map.set('a'.1)
map.set('b'.2)
map.set('c'.3)

Object.fromEntries(map) // [['a', 1], ['b', 2], ['c', 3]]
Copy the code

Note that if the key is of type number, the key will be converted to string. If the Map Object has a reference type (that is, a key type not accepted by Object), the key-value pair will be ignored.

For arrays, there are ambiguities in argument type derivation, so Entries or Entries generator objects can only be passed in

Object.fromEntries([1.2.3]) // Error: element type does not exist in [key, value] form

/ / only [[' 0 ', 1], [' 1 ', 2], [' 2 ', 3]] to conform to the nature of the Entries
Object.fromEntries([1.2.3].entries()) // [['a', 1], ['b', 2], ['c', 3]]
Object.fromEntries([...[1.2.3].entries()]) // [['a', 1], ['b', 2], ['c', 3]]
Copy the code

If a Map is a key-value store, why doesn’t the Map constructor take Object as an argument? In fact, the Map construct argument accepts Entries as objects of the same type as the Object.fromEntries argument:

const entries = [['a'.1], ['b'.2], ['c'.3]].new Map(entries) // Map(3) {"a" => 1, "b" => 2, "c" => 3}

const fd = new FormData()
fd.set('a'.1)
fd.set('b'.2)
fd.set('c'.3)

new Map(fd) // Map(3) {"a" => 1, "b" => 2, "c" => 3}

new Map([1.2.3].entries()) // Map(3) {0 => 1, 1 => 2, 2 => 3}
new Map([[...1.2.3].entries()]) // Map(3) {0 => 1, 1 => 2, 2 => 3}
Copy the code

As you can see here, Entries are a cheap version of Map, which encapsulates them so that they can be elegantly traversed, queried, retrieved or deleted.

Entries act as a middleman, allowing for the conversion of key-value pairs between objects that exist as key-value pairs.

summary

  • A duck type is a type derived from the behavior of an object
  • To determine ArrayLike, just check that the object haslengthProperty, and the length value is a number
  • To determine Iterable, check the objectSymbol.iteratorWhether the attribute value is a function
  • To determine Thenable, check objectsthenWhether the attribute value is a function
  • Entries are[key, value]As a two-digit array of elements, data structures such as objects, maps and FormData can be converted to each other using the properties of Entries.