一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
本题难度:⭐ ⭐
答:
注意:上图列举的是一般情况,如果使用 call 或者 apply 的场景非要用 bind 去实现,返回一个函数再执行,也是可以实现的,不过没必要。
call、apply 的应用场景
call 和 apply 的作用是相同的,只是调用时传参形式不同而已。
精准判断一个数据类型
精准地判断一个数据的类型,可以用到 Object.prototype.toString.call(xxx)。
调用该方法,统一返回格式 [object Xxx]的字符串,用来表示该对象。
// 直接调用
Object.prototype.toString({}) // '[object Object]'
// 加上 call
// 引用类型
Object.prototype.toString.call({}) // '[object Object]'
Object.prototype.toString.call(function(){}) // "[object Function]'
Object.prototype.toString.call(/123/g) // '[object RegExp]'
Object.prototype.toString.call(new Date()) // '[object Date]'
Object.prototype.toString.call(new Error()) // '[object Error]'
Object.prototype.toString.call([]) // '[object Array]'
Object.prototype.toString.call(new Map()) // '[object Map]'
Object.prototype.toString.call(new Set()) // '[object Set]'
Object.prototype.toString.call(new WeakMap()) // '[object WeakMap]'
Object.prototype.toString.call(new WeakSet()) // '[object WeakSet]'
Object.prototype.toString.call(document) // '[object HTMLDocument]'
Object.prototype.toString.call(window) // '[object Window]'
Object.prototype.toString.call(this) // '[object Window]'
// 原始类型
Object.prototype.toString.call(1) // '[object Number]'
Object.prototype.toString.call('1') // '[object String]'
Object.prototype.toString.call(true) // '[object Boolean]'
Object.prototype.toString.call(1n) // '[object BigInt]'
Object.prototype.toString.call(null) // '[object Null]'
Object.prototype.toString.call(undefined) // '[object Undefined]'
Object.prototype.toString.call(Symbol('a')) // '[object Symbol]'
为什么要调用 call
call 是函数的方法,用来改变 this 指向,用 apply 也可以。
如果不改变this指向为我们的目标变量,this将永远指向调用的Object.prototype。
Object.prototype.toString([]) // '[object Object]' this 指向 Object.prototype,判断类型为 Object。
Object.prototype.toString.call([]) // '[object Array]' this 指向 [],判断类型为 Array
// 重写 Object.prototype.toString 方法,把 this 打印出来看一下,进一步加深理解
Object.prototype.toString = function () {
console.log(this)
}
// 引用类型
Object.prototype.toString([]) // Object.prototype
Object.prototype.toString.call([]) // []
// 原始类型
Object.prototype.toString(1) // Object.prototype
Object.prototype.toString.call(1) // Number {1}
// 这里的 Number {1},是一个包装类,把基本类型用它们相应的引用类型包装起来,使其具有对象的性质
伪数组转数组
伪数组转数组,在 es6 之前,可以使用 Array.prototype.slice.call(xxx)
function add () {
const args = Array.prototype.slice.call(arguments)
// 也可以这么写 const args = [].slice.call(arguments)
args.push(1) // 可以使用数组上的方法了
}
add(1, 2, 3)
为什么要调用 call
和上文的判断数据类型的原理是一样的,如果不改变this指向为目标伪数组,this将永远指向调用的Array.prototype,就不会生效。
slice 方法原理
循环伪数组,把伪数组的元素挨个放到定义的一个新数组里,再返回新数组,差不多是类似这样的代码:
// 从 slice 方法原理理解为什么要调用 call
Array.prototype.slice = function (start, end) {
const res = []
start = start || 0
end = end || this.length
for (let i = start; i < end; i++) {
res.push(this[i]) // 这里的 this 就是伪数组,所以要调用 call
}
return res
}
ES5 实现继承
在一个子构造函数中,你可以通过调用父构造函数的 call 方法或者 apply 方法来实现继承。
function Person (name) {
this.name = name
}
function Student (name, grade) {
Person.call(this, name)
this.grade = grade
}
const p1 = new Person('lin')
const s1 = new Student('lin', 100)
上面的代码示例中,构造函数 Student 中会拥有构造函数 Person 中的 name 属性,grade 属性是 Student 自己的。
回调函数 this 丢失问题
执行下面的代码,回调函数会导致 this 丢失。
const obj = {
lastName: 'lin',
sayName() {
console.log(this.lastName)
}
}
obj.sayName() // 输出 'lin'
function fn(callback) {
if (typeof callback === 'function') {
callback()
}
}
fn(obj.sayName) // 输出 undefined
可以使用 call 或者 apply 改变 this 指向:
const obj = {
lastName: 'lin',
sayName() {
console.log(this.lastName)
}
}
obj.sayName() // 输出 'lin'
function fn(callback, context) { // 定义一个 context 参数,可以把上下文传进去
if (typeof callback === 'function') {
callback.apply(context) // 显式改变 this 值,指向传入的 context
}
}
fn(obj.sayName, obj) // 输出 'lin'
Vue 用 this.$options.data() 重置组件 data
用 this.$options.data() 重置组件 data 时,data() 里用 this 获取的 props 或 method 都为undefined,代码简化如下:
export default {
props: {
P: Object
},
data () {
return {
A: {
a: this.methodA
},
B: this.P
};
},
methods: {
resetData () { // 重置 data 时调用
Object.assign(this.$data, this.$options.data()) // 这么写就会出 bug
},
methodA () {
// do sth.
},
methodB () {
this.A.a && this.A.a(); // this.A.a is undefined, this.B is undefined!!!
}
}
}
调用resetData()之后,再调用methodB()时,this.A.a和this.B是undefined。
解决,resetData 里这么写:
resetData () {
Object.assign(this.$data, this.$options.data.call(this))
}
调用 this.$options.data.call(this),就可以把 props 和 methods 挂载上去。
我为啥连这么偏的知识都知道,因为我踩过坑,hhh。
至于为啥要这样重置一个组件的 data,因为实现起来简单,具体可以参考这篇文章:
获取数组最大值、最小值
要获取一堆数据中的最大值或者最小值,用 Math.max 或 Math.min 要一个一个地传递参数
要获取数组中的最大值,你可能会这么写:
const arr = [1,2,3,4,5]
function getArrMax (arr) {
let max = arr[0]
for (let i = 0; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i]
}
}
return max
}
console.log(getArrMax(arr)) // 5
实际上,用 apply 就可以很巧妙地把参数一个一个地传进 Math.max 中去:
const arr = [1,2,3,4,5]
console.log(Math.max.apply(Math, arr)) // 5
当然,es6 之后也可以用解构赋值轻松传值:
const arr = [1,2,3,4,5]
console.log(Math.max(...arr)) // 5
将数组各项添加到另一个数组
const arr1 = [1, 2, 3]
const arr2 = ['a', 'b', 'c']
这样两个数组,如何把 arr2 的数组项添加到 arr1。
用数组解构可以轻松解决
const arr1 = [1, 2, 3]
const arr2 = ['a', 'b', 'c']
arr1.push(...arr2)
console.log(arr1) // [1,2,3,'a','b','c']
但是 es6 之前可没有解构赋值,怎么办?
用 concat 不行,因为 concat 不能改变原数组,难道又要使用循环了吗?
其实和获取数组最大值、最小值是一样的,也可以使用 apply 来解决。
const arr1 = [1, 2, 3]
const arr2 = ['a', 'b', 'c']
arr1.push.apply(arr1, arr2)
console.log(arr1)
利用的也是 apply 可以把多个参数转化为一个参数数组的原理。
bind 应用场景
解决循环陷阱问题
一个非常经典的循环陷阱问题,下面这段代码打印出来的值都是 5。
const arr = []
for (var i = 0; i < 5; i++) {
arr.push(function () {
console.log(i)
})
}
arr[0]() // 5
arr[1]() // 5
arr[2]() // 5
这个问题除了用闭包来解决,也可以用 bind 函数来解决。
bind 函数可以显式改变 this 指向,会返回一个新的函数,我们利用 bind 函数会返回一个新函数的特性,来解决循环陷阱的问题。
const arr = []
for (var i = 0; i < 5; i++) {
arr.push(function (i) {
console.log(i)
}.bind(this, i)) // 不用改变 this 指向,我们主要是为了返回一个新的函数
}
arr[0]() // 0
arr[1]() // 1
arr[2]() // 2
关于循环陷阱,更多可参考我的这篇文章:
react 的 this 指向问题
import React from 'react'
class App extends React.Component{
handleClick() {
console.log(this); // 输出 undefined
}
render(){
return (
<button onClick={this.handleClick}>
Click Me
</button>
);
}
}
export default App
执行 handleClick 方法时,输出的 this 是 undefined,和预期的有点不一样啊。
如何解决 this 指向 undefined 问题
可以使用 bind 手动绑定或者使用箭头函数。
import React from 'react'
class App extends React.Component{
handleClick() {
console.log(this); // 输出 App
}
render(){
return (
// 使用 bind
<button onClick={this.handleClick.bind(this)}>
Click Me
</button>
)
}
}
export default App
import React from 'react'
class App extends React.Component{
// 使用箭头函数
handleClick = () => {
console.log(this); // 输出 App
}
render(){
return (
// 使用 bind
<button onClick={this.handleClick}>
Click Me
</button>
)
}
}
export default App
结尾
阿林水平有限,文中如果有错误或表达不当的地方,非常欢迎在评论区指出,感谢~
如果我的文章对你有帮助,你的👍就是对我的最大支持^_^
你也可以关注《前端每日一问》这个专栏,防止失联哦~
我是阿林,输出洞见技术,再会!
上一篇:
「前端每日一问(37)」call、apply 和 bind 的区别是什么?
下一篇: