xccjk / x-blog

学习笔记
18 stars 2 forks source link

浅谈JavaScript中call与apply的模拟实现 #56

Closed xccjk closed 2 years ago

xccjk commented 3 years ago

浅谈JavaScript中call与apply的模拟实现

call

call()方法是使用一个指定的this值和若干个指定的参数值的前提下调用某个函数或方法

语法:

function.call(thisArg, arg1, arg2, ...)

demo:

var foo = {
    name: 'xcc'
}
function bar (age) {
    console.log(`name: ${this.name}, age: ${age}`)
}
bar.call(foo, 20)    // name: xcc, age: 20

注意事项:

  1. call改变了this的指向,由bar指向了foo
  2. bar函数执行了

模拟实现

第一步:

试想当调用call时,把foo对象改造如下:

var foo = {
    name: 'xcc',
    bar: function() {
        console.log(this.name)
    }
}

带来的问题,给foo函数引入了对于的属性bar

解决方式:通过delete方法删除多余属性

模拟步骤分为下面几部:

  1. 将函数设置为对象的属性
  2. 执行该函数
  3. 删除该函数

等价于下面流程:

// 第一步
foo.fn = bar
// 第二步
foo.fn()
// 第三步
delete foo.fn

实现方式:

// 第一版
Function.prototype.call2 = function(context) {
    // 首先获取调用call的函数,通过this获取
    context.fn = this
    context.fn()
    delete context.fn
}

var foo = {
    name: 'xcc'
}

function bar() {
    console.log(this.bar)   // xcc
}

bar.call2(foo)

第二步

给定call函数执行时添加参数执行

var foo = {
    name: 'xcc'
}

function bar(age) {
    console.log(this.name, age)
}

bar.call(foo, 20)   // xcc 20
// 第二版
Function.prototype.call2 = function(context) {
    context.fn = this || window
    var args = []
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']')
    }
    eval('context.fn(' + args +')')
    delete context.fn
}

var foo = {
    value: 1
}

function bar(name, age) {
    console.log(this.value, name, age)  // 1 xcc 20
    return {
        value: this.value,
        name: name,
        age: age
    }
}

bar.call2(foo, 'xcc', 20)

注意事项:

  1. call()方法第一个值为null或者undefined时,非严格模式下指向window
  2. 函数是可以有返回值的
  3. 自定义call函数时入参的处理方式

apply

apply()方法调用一个具有给定this值的函数,以及以一个数组(或类数组对象)的形式提供的参数

语法:

func.apply(thisArg, [argsArray])

demo:

var foo = {
    name: 'xcc'
}
function bar (nums) {
    console.log(this.name, nums)    // xcc 20
}
bar.apply(foo, [20])

实现方式

Function.prototype.apply = function (context, arr) {
    var context = Object(context) || window
    context.fn = this

    var result
    if (!arr) {
        result = context.fn()
    }
    else {
        var args = []
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']')
        }
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result
}

参考资料

xccjk commented 2 years ago

this的指向问题 - 探究this指向原理

先说结论:

this永远指向最后调用它的那个对象

this永远指向最后调用它的那个对象

this永远指向最后调用它的那个对象

var name = 'globalName'

function fn() {
  console.log(this.name)
}

fn()  // globalName

我们发现,最后调用fn的位置就是fn()前面没有对象。在非严格模式下,调用对象就是全局对象window.fn()。在严格模式下,会抛出错误。

var name = 'globalName'

var obj = {
  name: 'xcc',
  fn: function() {
    console.log(this.name)
  }
}

obj.fn() // 'xcc'

在这个例子中, 会发现函数fn是对象obj调用的,所以this指向obj

var name = 'globalName'

var obj = {
  name: 'xcc',
  fn: function() {
    console.log(this.name)
  }
}

window.obj.fn() // 'xcc'

在这个例子中,会发现结果还是xcc,因为this指向永远只与最后调用它的那个对象有关,不管对象obj前面有多少对象,都与函数fn中的this指向没关系

var name = 'globalName'

var obj = {
  fn: function() {
    console.log(this.name)
  }
}

obj.fn() // undefined

还是可以发现,this还是指向最后调用它的那个对象obj,obj中没有定义name属性,因此结果为undefined

下面看一个存在坑的例子

var name = 'globalName'

var obj = {
  name: 'xcc',
  fn: function() {
    console.log(this.name)
  }
}

var f = obj.fn

f() // 'globalName'

看到这个例子,发现为什么不是打印xcc呢,因为在声明var f = obj.fn时并没有发生调用,而是以window.f()调用的,因此this会指向window

var name = 'globalName'

var obj = {
  name: 'xcc',
  fn: function() {
    console.log(this.name)
  }
}

var f = obj.fn() // 'xcc'

会发现在声明var f = obj.fn()时直接调用了obj.fn(),因此this会指向obj

var name = 'globalName'

function fn() {
  var name = 'xcc'
  fn1()
  function fn1() {
    console.log(this.name)
  }
}

fn() // 'globalName'

这个例子中,函数fn中的fn1在调用时,前面并没有对象

作为函数方法调用函数时:

在 JavaScript 中, 函数是对象。 JavaScript 函数有它的属性和方法。call() 和 apply() 是预定义的函数方法。 两个方法可用于调用函数,两个方法的第一个参数必须是对象本身 在 JavaScript 严格模式(strict mode)下, 在调用函数时第一个参数会成为 this 的值, 即使该参数不是一个对象。在 JavaScript 非严格模式(non-strict mode)下, 如果第一个参数的值是 null 或 undefined, 它将使用全局对象替代。

因此函数fn1在非严格模式下等价于window.fn1()