JavaScript的参数传递与手写call,apply,bind,new

218 阅读2分钟

js参数传递

先贴一段《JavaScript高级程序设计》的介绍

ECMAScript 中所有函数的参数都是按值传递的。这意味着函数外的值会被复制到函数内部的参数中,就像从一个变量复制到另一个变量一样。如果是原始值,那么就跟原始值变量的复制一样,如果是引用值,那么就跟引用值变量的复制一样。

原始类型(按值传递)

function addTen(num) {
    num += 10;
    console.log(num); // 30
}
let count = 20;

addTen(count);
console.log(count); // 20

引用类型(按共享传递)

// case1
function changeName(obj) {
    obj.name = 'newName'
    console.log(obj.name); // 'newName'
}
let person = {
    name: 'cx'
}

changeName(person);
console.log(person.name); // 'newName'

// case2
function changeName(obj) {
    obj = { name: 'newName' }
    console.log(obj.name); // 'newName'
}
let person = {
    name: 'cx'
}

changeName(person);
console.log(person.name); // 'cx'

那为什么case1能按引用传递呢?因为基本类型值存储于栈内存中,传递的就是当前值,修改不会影响原有变量的值。而引用类型在栈中存的是地址,传递的是地址索引,修改的是该地址在堆内存中的值。例子2中将obj重新赋值为另一个地址,所以它不会影响到person对象。

call

分析call

具体用法

function showVal() {
    console.log('实参', ...arguments);
    return `当前值是${this.value}`
}

var value = 2
const obj = {
    value: 1,
}
showVal.call(obj,  { aa: 111 }, { b: 222 })  // // 实参 { aa: 111 } { b: 222 } 当前值是1
showVal.call(null,  { aa: 111 }, { b: 222 }) //  // 实参 { aa: 111 } { b: 222 } 当前值是2

简要分析

  1. 改变了this指向
  2. 运行了showVal函数
  3. 能传递指定参数
  4. 不传this参数时,this指向全局(浏览器指向window,node环境指向global / globalThis

实现call

步骤

  1. 将调用的函数设置为对象的属性
  2. 执行该函数并返回值
  3. 删除该函数

实现

    Function.prototype.myCall1 = function (context) {
        let args = Array.prototype.slice.call(arguments, 1)
        context ??= globalThis
        context.fn = this
        let result = context.fn(...args)
        delete context.fn
        return result
    }

    //测试
    window.value = 2
    function showVal() {
        console.log('实参', ...arguments);
        return `当前值是${this.value}`
    }

    const obj = {
        value: 1,
    }

    console.log(showVal.myCall1(obj, { aa: 111 }, { b: 222 })); // 实参 { aa: 111 } { b: 222 } 当前值是1
    console.log(showVal.myCall1(null, { aa: 111 }, { b: 222 })); // 实参 { aa: 111 } { b: 222 } 当前值是2

优化

Function.prototype.myCall2 = function (context, ...args) {
    context ??= globalThis
    let fnSymbol = Symbol() // 保证键的唯一性
    context[fnSymbol] = this
    let result = context[fnSymbol](...args)
    delete context[fnSymbol]
    return result
}

apply

分析与实现

与call类似,不过入参为数组,若不是数组则直接调用函数且参数为非对象类型时报错

    Function.prototype.myApply = function (context, args) {
        if (typeof args !== 'object') throw new TypeError('CreateListFromArrayLike called on non-object')
        context ??= globalThis
        args = Array.isArray(args) ? args : []
        let fnSymbol = Symbol()
        context[fnSymbol] = this
        let result = context[fnSymbol](...args)
        delete context[fnSymbol]
        return result
    }

    window.value = 2
    function showVal() {
        console.log('实参', ...arguments);
        return `当前值是${this.value}`
    }

    const obj = {
        value: 1,
    }

    console.log(showVal.apply(obj, { aa: 111 }, { b: 222 })); // 实参  当前值是1

    console.log(showVal.myApply(obj, [{ aa: 111 }, { b: 222 }])); // 实参 { aa: 111 } { b: 222 } 当前值是1
    console.log(showVal.myApply(null, { aa: 111 }, { b: 222 })); // 实参  当前值是2

bind

分析bind

具体用法

var value = 2
function showVal() {
    console.log('实参', ...arguments);
    return `当前值是${this.value}`
}

const obj = {
    value: 1,
}

const _bind = showVal.bind(obj, 'params1')
console.log(_bind('params2')); // 实参 params1 params2;  当前值是1

简要分析

  1. 改变this指向,返回一个函数
  2. bind函数和它返回的函数都可以接收参数

实现bind

Function.prototype.myBind = function (context) {
    const self = this
    let args = Array.prototype.slice.call(arguments, 1)
    return function () {
        let bindArgs = Array.prototype.slice.call(arguments)
        return self.apply(context, args.concat(bindArgs))
    }
}

// rest参数写法
Function.prototype.myBind2 = function (context, ...args) {
    return (...args2) => {
        return this.call(context, ...args, ...args2)
    }
}

window.value = 2
function showVal() {
    console.log('实参', ...arguments);
    return `当前值是${this.value}`
}

const obj = {
    value: 1,
}

const _bind = showVal.myBind(null, 'params1')
console.log(_bind('params2')); // 实参 params1 params2;  当前值是1

构造函数效果模拟

一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

也就是当bind返回的函数当构造函数使用时,调用bind时传入的this会失效,但其他参数依然会生效

bind演示

window.value = 2
function showVal(name, age) {
    this.name = name
    console.log('值:', this.value, '参数', arguments);
}
const obj = {
    value: 1,
}

const newBind = showVal.bind(obj, 'cx')
newBind('params2') //  值: 1 参数: Arguments(2) ['cx', 'params2']
console.log('----------------');
const foo = new newBind(24)   // 值: undefined 参数: Arguments(2) ['cx', 24]
console.log(foo);  // showVal { name: 'cx' }

构造函数效果实现

function showVal(name, age) {
    this.name = name
    console.log('值:', this.value, '参数', arguments);
}
showVal.prototype.xxx = 'xxx'

const obj = {
    value: 1,
}

Function.prototype.myBind = function (context, ...args) {
    const self = this
    //空函数进行中转
    const Fntemp = function () { }
    
    const FBound = function (...args2) {
        return self.apply(this instanceof Fntemp ? this : context, [...args, ...args2])
    }

    // FBound.prototype = this.prototype // 直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype
    Fntemp.prototype = this.prototype
    FBound.prototype = new Fntemp()

    return FBound
}

const newBind = showVal.myBind(obj, 'cx')
newBind('params2')  //  值: 1 参数: Arguments(2) ['cx', 'params2']
console.log('----------------');
const foo = new newBind(24)    // 值: undefined 参数: Arguments(2) ['cx', 24]
console.log(foo); // FBound {name: 'cx'}

new

new的过程

《JavaScript高级程序设计》里是这么介绍的

  1. 在内存中创建一个新对象
  2. 这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype属性
  3. 构造函数内部的this被赋值为这个新对象(即this指向新对象)
  4. 执行构造函数内部的代码(给新对象添加属性)
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象

实现new

function newFn(fatherFn, ...args) {
    if (typeof fatherFn !== 'function') return `the frist param must be a function`
    let obj = Object.create(fatherFn.prototype)
    let res = fatherFn.call(obj, ...args)
    return res instanceof Object ? res : obj
}

function Person(name, age) {
    this.name = name
    this.age = age
    this.xxx = 'xxx'
    return null
}
Person.prototype.say = function () {
    console.log(`I am ${this.name}, I'm ${this.age} years old. `);
}
const person1 = newFn(Person, 'cx', 24)
console.log(person1);  // Person { name: 'cx', age: 24, xxx: 'xxx' }
person1.say() // I am cx, I'm 24 years old.