Suppose we have an array of strings [' some ', ' strings '] and we need to clear the whitespace before and after the strings, the first thought is.

1
[' some ', ' strings '].map(s => s.trim())

For further optimization, consider removing the wrapped function from the map and using String.prototype.trim directly. however, the problem arises.

1
2
3
[' some ', ' strings '].map(String.prototype.trim)
// TypeError: String.prototye.trim called on null or undefined (Chrome)
// TypeError: can't convert undefined to object (Firefox)

The second argument of map

The reason for the error is that Array.prototype.map has a second argument that is easily overlooked.

1
2
3
var new_array = arr.map(function callback(currentValue[, index[, array]]) {
    // Return element for new_array
}[, thisArg])

where thisArg specifies the value of this to be bound when the callback is executed, and we print this this out as follows

1
2
3
4
5
6
7
8
9
[' some ', ' strings '].map(function (s) {
  console.log(this) // 全局对象或者 undefined
  return s
}) // [' some ', ' strings ']

[' some ', ' strings '].map(function (s) {
  console.log(this) // 'hello world'
  return s
}, 'hello world') // [' some ', ' strings ']

JS’s methods and prototype

We know that trim, map, and other methods are defined in built-in objects such as String, Array, and so on. The implementation of these methods is integrated into the JS runtime environment. Here is a reference to a polyfill of String.prototype.trim.

1
2
3
String.prototype.trim = function () {
  return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
};

When we call " str ".trim(), the string literal is converted to a String object and the . operator binds this object to the this value of String.prototype.trim() and executes this function. As you can see, the string that trim operates on is the value of this, not the argument passed in to the function. Without explicit binding, it appears that this in functions defined on the built-in object prototype will have a default initial value. For example.

1
2
3
4
5
6
7
String.prototype.r = function () { return this }
String.prototype.r() // String { "" }
String.prototype.r() instanceof String // 但这里是 false

// trim 操作的是 this 的值,并非传入的参数
String.prototype.trim() // ""
String.prototype.trim(" abc ") // ""

The second parameter in Array.prototype.map is just another way to bind this, usually we don’t pass this parameter, and this in the map callback function is undefined. this in String.prototype.trim is tied to undefined, resulting in a TypeError, which is equivalent to the following case.

1
2
String.prototype.trim.bind(undefined)()
// TypeError: String.prototye.trim called on null or undefined

Function.prototype.call

There is one solution.

1
2
[' some ', ' strings '].map(Function.prototype.call, String.prototype.trim)
// ['some', 'strings']

Function.prototype.call is another way to modify the this bound to a function call.

1
2
3
4
" str".trim() // "str"
// 等价于以下函数调用
String.prototype.trim.call(" str") // "str"
String.prototype.trim.bind(" str")() // "str

We pass Function.prototype.call in the first parameter of the map, where this is bound to the second parameter passed String.prototype.trim, similar to the following code.

1
2
3
4
[' some ', ' strings '].map(function (s) {
  console.log(this) // String.prototype.trim
  return this.call(s)
}, String.prototype.trim)

Summary

The above solution is fantastic, and the actual development should avoid similar enigmatic code. The recommended method for this specific scenario is [' some ', ' strings '].map(s => s.trim().

However, learning about this issue helps to deepen your understanding of JS prototype and language features such as functions.

We generally refer to functions that “belong” to an object as methods. JS implements such a mechanism semantically through the prototype mechanism. In practice, however, a method defined in an object is not fundamentally different from a normal function, except that the object happens to hold a reference to that function. Even if a normal function can manipulate this, there is no direct connection between the value bound to this and the object it contains.

JS’s this, prototype, and other mechanisms should be designed to implement the object-oriented paradigm in dynamic languages. Functions (methods) implement object-oriented “artifacts” by manipulating this instead of the function’s arguments, and by calling methods like s.trim(). A recent look at Python’s classes shows some similarities. The functions in a class are related to each other only by self, very similar to JS’s this, except that Python’s “method” lists self explicitly as an argument to a function, whereas JS’s this is implicit and can be tampered with in some special way. JS’s this is implicit and can be tampered with in some special way, making it somewhat elusive.

1
2
3
4
5
class SomeJob():
  def __init__(self, date):
    self.date = date
  def run(self):
    print(self.date)

map is the classic set of functional styles. Ideally, a map function should be a pure function, e.g. it is more elegant to call jQuery’s trim function.

1
[' some ', ' strings '].map($.trim)

However, String.prototype.trim in the JS standard library operates on this and is not a pure function, which leads to the problem described in this article.

Legacy issues

Testing in Firefox 61 it seems that the second argument of map is invalid.