为何 JS 的 map 中不能使用 String.prototype.trim?
June 02, 2018
假设有一个元素为字符串的数组 [' some ', ' strings ']
,我们需要清除字符串中前后空白字符,首先想到:
[' some ', ' strings '].map(s => s.trim())
进一步优化,考虑将 map 中包裹的函数去掉,直接使用 String.prototype.trim。然而问题出现了:
[' some ', ' strings '].map(String.prototype.trim)
// TypeError: String.prototye.trim called on null or undefined (Chrome)
// TypeError: can't convert undefined to object (Firefox)
map 的第二个参数
造成错误的原因在于 Array.prototype.map 有一个易被忽视的第二个参数:
var new_array = arr.map(function callback(currentValue[, index[, array]]) {
// Return element for new_array
}[, thisArg])
其中的 thisArg 指定了执行 callback 时要绑定的 this 的值,我们把这个 this 打印出来:
[' 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 的 方法 与 prototype
String.prototype.trim = function () {
return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
};
当我们调用" str ".trim()
的时候,字符串字面量被转成 String 对象,.
操作符将此对象绑定为 String.prototype.trim() 的 this 值并执行此函数。
可以看到,trim 操作的字符串是 this 的值,而非函数传进来的参数。如果不显式地进行绑定,内置对象 prototype 上定义的函数中的 this 似乎会有个默认的初始值。比如:
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 ") // ""
Array.prototype.map 中的第二个参数正好又是另一种绑定 this 的方式,通常我们不传这个参数,map 回调函数中的 this 为 undefined。String.prototype.trim 中的 this 被绑成了 undefined,导致了 TypeError,相当于如下情况:
String.prototype.trim.bind(undefined)()
// TypeError: String.prototye.trim called on null or undefined
Function.prototype.call
有一种解法:
[' some ', ' strings '].map(Function.prototype.call, String.prototype.trim)
// ['some', 'strings']
Function.prototype.call 是另一种修改函数调用时绑定的 this 的方法:
" str".trim() // "str"
// 等价于以下函数调用
String.prototype.trim.call(" str") // "str"
String.prototype.trim.bind(" str")() // "str
我们在 map 的第一个参数中传入 Function.prototype.call,其中的 this 被绑定到传入的第二个参数 String.prototype.trim,类似如下代码:
[' some ', ' strings '].map(function (s) {
console.log(this) // String.prototype.trim
return this.call(s)
}, String.prototype.trim)
总结
上述的解法非常奇妙,实际开发中还是应当避免类似谜一般的代码。这个具体的使用场景推荐使用 [' some ', ' strings '].map(s => s.trim())
这种方法。
s.trim()
class SomeJob():
def __init__(self, date):
self.date = date
def run(self):
print(self.date)
map 是函数式的经典套路。理想情况下 map 的函数应当是一个纯函数,比如调用 jQuery 的 trim 函数会显得更加优雅一些:
[' some ', ' strings '].map($.trim)
遗留问题
在 Firefox 61 中测试似乎 map 的第二个参数是无效的。
参考资料
- javascript - Why won’t passing
''.trim()
straight to[].map()
’s callback work? - Stack Overflow Array.prototype.map() - JavaScript | MDN String.prototype.trim() - JavaScript | MDN - Seva Zaikov - The Most Clever Line of JavaScript
ECMAScript 2015 Language Specification – ECMA-262 6th Edition