函数

24 阅读21分钟

原生/内建函数

常用的原生函数:

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Error()
  • Symbol()

原生函数可以被当作构造函数来使用,但其构造出来的对象可能会和设想的有所出入:

 var a = new String('abc')
 typeof a // 'object'
 a instanceof String // true
 Object.prototype.toString.call(a) // '[object String]'

通过构造函数(如 new String(‘abc’))创建出来的是封装了基本类型值(如‘abc’)的封装对象。

如果想要自行封装基本类型值,可以使用Object(..) 函数(不带new 关键字)。

拆封

如果想要得到封装对象中的基本类型值,可以使用valueOf() 函数。

 // 在需要用到封装对象中的基本类型值的地方会发生隐式拆封
 var a = new String('abc')
 var b = new Number(42)
 var c = new Boolean(true)
 ​
 a.valueOf() // 'abc'
 b.valueOf() // 42
 c.valueOf() true

函数实际上是对象。每个函数都是Function 类型的实例,而Function 也有属性和方法,跟其他引用类型一样。因为函数是对象,所以函数名就是指向函数对象的指针,而且不一定与函数本身紧密绑定。

定义函数

方式

  • 函数声明

  • 函数表达式(函数字面量)

  • 箭头函数(lambda 函数),ES6 新增的JS 标准 => 以尽量简洁的语法定义函数

  • 使用Function 构造函数

    let sum = new Function(num1, num2) {
        return num1 + num2
    }
    
    // 不推荐使用这种语法来定义函数,因为这段代码会被解释两次:第一次是将它当作常规ES 代码,第二次是解释传给构造函数的字符串。这显然会影响性能。
    
  • 生成器函数,ES6 新增功能,可以创建不同于普通函数的函数,在应用程序执行过程中,这种函数能够退出再重新进入,在这些再进入之间保留函数内变量的值。

    function* myGen() { yield 1; }
    

new调用构造函数

函数内部会发生如下变化:

  • 创建一个this 变量,该变量指向一个空对象,并且该对象继承函数的原型;
  • 属性和方法被加入到this 引用的对象中;
  • 隐式返回this 对象(如果没有显性返回其他对象)
// 伪程序
function Person(name){
  // 创建this 变量,指向空对象
  var this = {}
  // 属性和方法被加入到this 引用的对象中
  this.name = name
  this.say = function(){
    return "I am " + this.name
  }
  // 返回this对象
  return this
}

new调用构造函数,最大特点为this对象指向构造函数生成的对象

// 如果指定了返回对象,this 对象可能被丢失。

function Person(name){
  	this.name = name
  	this.say = function(){
    	return "I am " + this.name
  	}
  	let that = {}
  	that.name = "It is that!"
  	return that
}

let person1 = new Person('lucius')
person1.name // "It is that!"

如果直接调用函数,那么,this对象指向window,并且不会默认返回任何对象(除非显性声明返回值)。

function Person(name){
  	this.name = name;
  	this.say = function(){
    	return "I am " + this.name
  	}
}

let person1 = Person('lucius')
person1 // undefined
window.name // lucius
// 京东
function Foo() {
    Foo.a = function() {
        console.log(1)
    }
    this.a = function() {
        console.log(2)
    }
}
// 以上只是 Foo 的构建方法,没有产生实例,此刻也没有执行

Foo.prototype.a = function() {
    console.log(3)
}
// 现在在 Foo 上挂载了原型方法 a ,方法输出值为 3

Foo.a = function() {
    console.log(4)
}
// 现在在 Foo 上挂载了直接方法 a ,输出值为 4

Foo.a()
// 立刻执行了 Foo 上的 a 方法,也就是刚刚定义的,所以
// # 输出 4

let obj = new Foo()
/* 这里调用了 Foo 的构建方法。Foo 的构建方法主要做了两件事:
1. 将全局的 Foo 上的直接方法 a 替换为一个输出 1 的方法。
2. 在新对象上挂载直接方法 a ,输出值为 2。
*/

obj.a()
// 因为有直接方法 a ,不需要去访问原型链,所以使用的是构建方法里所定义的 this.a,
// # 输出 2

Foo.a()
// 构建方法里已经替换了全局 Foo 上的 a 方法,所以
// # 输出 1

函数声明 vs 函数表达式

JS 引擎在加载数据时对两者区别对待的:

函数声明会被提升,但是函数表达式却不会被提升。 】 => JS 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。

// 没问题
console.log(sum(10, 10))
function sum() {
    return num1 + num2
}

// 会出错
console.log(sum(10, 10))
let sum = function sum() {
    return num1 + num2
}

// 函数定义包含在一个变量初始化语句中,而不是函数声明中。
// 这并不是因为使用let 而导致,使用var 关键字也会碰到相同的问题。

函数声明会在任何代码执行之前读取并添加到执行上下文。这个过程叫做函数声明提升(function declaration hoisting)。

函数只会在第一次执行的时候被编译,所以编译时变量环境和词法环境最顶层数据已经确定了。

当执行到块级作用域的时候,块级作用域中通过let和const申明的变量会被追加到词法环境中,当这个块执行结束之后,追加到词法作用域的内容又会销毁掉。

console.log(a)
var a = 1
var getNum = function() {
  a = 2
}
function getNum() {
  a = 3
}
console.log(a)
getNum()
console.log(a)

// undefined、1、2
  • 首先因为var 声明的变量提升作用,所以a 变量被提升,未赋值,所以第一个打印出来的是undefined。

  • 接下来是函数声明和函数表达式的区别:

    • 函数声明会有提升作用,在代码执行前就把函数提升到顶部,在执行上下文上中生成函数定义,所以第二个getNum 会被最先提升。
    • 然后是var 声明getNum 的提升,但是因为getNum 函数已经被声明了,所以就不需要再声明一个同名变量,接下来开始执行代码,执行到var getNum = fun...时,虽然声明被提前了,但是赋值操作还是留在这里,所以getNum 被赋值为了一个函数,下面的函数声明直接跳过。
    • 最后,getNum 函数执行前a 打印出来还是1,执行后,a 被修改成了2,所以最后打印出来的2。
函数优先

函数声明和var 声明都会发生提升,但是函数会优先提升,所以如果变量和函数同名的话,变量的提升就忽略了。但出现在后面的函数声明还是可以覆盖前面的。

// e.g.1
console.log(a) // function a() {}
var a = 1
function a(){}
console.log(a) // 1

// e.g.2
var b
function b(){}
console.log(b) // f b() {}

// e.g.3
function b(){}
var b
console.log(b) // f b() {}
var a = 0
console.log(a, window.a) // 0, 0
if (true) {
    console.log(a, window.a) // func, 0
    a = 10
    console.log(a, window.a) // 10, 0
    function a() {} // 提升,并且这里会有一个隐式操作,将当前a的值赋值给到全局变量a
    console.log(a, window.a) // 10, 10
    a = 20
    console.log(a, window.a) // 20, 10
}
console.log(a, window.a) // 10, 10

image-20220106142703173

function Foo() {
  getName = function () { console.log(1) }
  return this
}
Foo.getName = function () { console.log(2) }
Foo.prototype.getName = function () { console.log(3) }
var getName = function () { console.log(4) }
function getName() { console.log(5) }

//请写出以下输出结果:
Foo.getName()
getName()
Foo().getName()
getName()
new Foo.getName()
new Foo().getName()
new new Foo().getName()

// 2、4、1、1、2、3、3
  1. 执行Foo 函数的静态方法,打印出2。
  2. 执行getName,当前getName是打印出4 的那个函数。
  3. 执行Foo 函数,修改了全局变量getName,赋值成了打印1 的函数,然后返回this,因为是在全局环境下执行,所以this 指向window,因为getName 已经被修改了,所以打印出1。
  4. 因为getName 没有被重新赋值,所以再执行仍然打印出1。
  5. new 操作符是用来调用函数的,所以new Foo.getName()相当于new (Foo.getName)(),所以new 的是Foo 的静态方法getName,打印出2。
  6. 因为点运算符(.)的优先级和new 是一样高的,所以从左往右执行,相当于(new Foo()).getName(),对Foo使用new 调用会返回一个新创建的对象,然后执行该对象的getName 方法,该对象本身并没有该方法,所以会从Foo 的原型对象上查找,找到了,所以打印出3。
  7. 和上题一样,点运算符(.)的优先级和new 一样高,另外new 是用来调用函数的,所以new new Foo().getName()相当于new ((new Foo()).getName)(),括号里面的就是上一题,所以最后找到的是Foo 原型上的方法,无论是直接调用,还是通过new 调用,都会执行该方法,所以打印出3。

箭头函数

ES6 新增项。

Arrow function 总是用字面量声明语法来声明的,但它不是声明语句,只能作为表达式的操作数,并以表达式所在的上下文作为它的执行环境。

箭头函数是匿名函数,因为它的声明无法声明标识符。

语法:( paramerters ) => { functionBody }

表达形式
  • (...args) => expression

    args 表示参数有0个、1个、多个;expression 表示一个js 表达式,只有一行代码,如赋值操作、表达式,而且省略大括号会隐式返回这行代码的值。

    var samurai = (() => 'Tomoe')() // 'Tomoe'
    var ninja = (() => {'Yoshi'})() // undefined
    
  • (...args) => { body }

    args 表示参数有0、1、多个;body 表示有多行代码,最后一行必须是 return 语句。

// 用于条件运算符
let welcome = (age < 18) ? () => alert('Hello') : () => alert('Greetings!')

welcome()

如果箭头函数的函数体只有一句代码,就是 返回一个对象,用小括号包裹要返回的对象,不报错

const getTempItem = id => ({ id: id, name: "Temp" })

如果箭头函数的函数体只有一条语句并且不需要返回值(最常见是调用一个函数),可以给这条语句前面加一个void关键字

const fn = () => void doesNotReturn()
重要特性

1、函数体内的this 对象是定义时所在的作用域中的this 值,而不是使用时所在的对象

2、没有prototype 属性

3、不可以当作构造函数,不可以使用new 命令,否则抛出错误

  • 没有自己的this,无法调用call,apply
  • 没有prototype 属性,而new 命令在执行时需要将构造函数的prototype 赋值给新的对象的__proto__

【实现new】

new 操作符新建了一个空对象,这个对象原型指向构造函数的prototype,执行构造函数后返回这个对象。

function myNew() {
  const obj = {}
  const con = [].shift.call(arguments)
  obj.__proto__ = con.prototype
  const res = con.apply(obj, arguments)
  return res instanceof Object ? res : obj
}

/* 以下是对实现的分析:
- 创建一个空对象
- 获取构造函数
- 设置空对象的原型
- 绑定 this 并执行构造函数
- 确保返回值为对象 */

4、arguments、super、new target 三个变量在箭头函数中不存在,分别指向外层函数的对应变量

5、不可以使用yield 命令,因此箭头函数不能用作Generator 函数

函数名就是指向函数的指针,所以它们跟其他包含对象指针的变量具有相同的行为。这意味着一个函数可以有多个名称。

function sum(num1, num2) {
    return num1 + num2
}
console.log(sum(10, 10))        // 20
let anotherSum = sum 
console.log(anotherSum(10, 10)) // 20
sum = null
console.log(anotherSum(10, 10)) // 20
console.log(sum(10, 10))        // 20

参数

  • 实参(argument):调用函数时传递给函数的值
  • 形参(parameter):定义函数所列举的变量

没有重载

在其他语言如Java 中,一个函数可以有两个定义,只要签名(接收参数的类型和数量)不同就行。ES 函数没有签名,因为参数是由包含零个或多个值的数组表示的。没有函数签名,自然也就没有重载。

如果在ES 中定义了两个同名函数,则后定义的会覆盖先定义的。

console.log(typeof fun === 'function', 'we access the function') // fun 指向一个函数
var fun = 3
console.log(typeof fun === 'number', 'Now we access the number') // fun 指向一个数字
function fun() {}
console.log(typeof fun === 'number', 'Still a number') // fun 仍然指向数字

默认参数值

支持显式定义默认参数。

在使用默认参数时,arguments 对象的值不反映参数的默认值,只反映传给函数的参数。

跟ES5 严格模式一样,修改命名参数也不会影响arguments 对象,它始终以调用函数时传入的值为准

默认参数作用域与暂时性死区:

  • 给多个参数定义默认值实际上跟使用let 关键字顺序声明变量一样。
function makeKing(name = 'Lucius', numerals = 'VIIT') {
    return `King ${name}  ${numerals}`
}

function makeKing() {
    let name = 'Lucius'
    let numerals = 'VIIT'
    return `King ${name}  ${numerals}`
}

image-20220729165228351

  • 参数按顺序初始化,所以后定义默认值的参数可以引用先定义的参数。
  • 参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的,否则会抛出错误。

image-20220729164254052

剩余参数

rest parameters 已被加入ES6 标准。

扩展操作符在实参列表和形参列表中均可出现:

  • 当出现在实参列表中时,被称为spread 参数,会将一个数组展开,并将数组中的每个元素依次作为传入函数对应位置的实参;
  • 当出现在形参列表中时,被称为rest 参数,从在实参中传入的当前位置起,所有参数都将被合成一个数组中的对应元素以供函数体使用,rest 参数必须是整个参数列表的最后一个参数。

arguments 对象只是消费扩展操作符(...)的一种方式。

在普通函数和箭头函数中,也可以将扩展操作符用于命名参数,当然同时也可以使用默认参数。

在构思函数定义时,可以使用扩展操作符把不同长度的独立参数组合成一个数组。收集参数的前面如果还有命名参数,则只会收集其余的参数;如果没有则会得到空数组。

箭头函数虽然不支持arguments 对象,但支持收集参数的定义方式。

let getSum = (...values) => {
		return values.reduce((x, y) => x + y, 0)
}

ECMAScript 中所有函数的参数都是按值传递的。

function setName(obj) {
    obj.name = 'Nicholas'
}
let person = new Object()
setName(person)
console.log(person.name) // 'Nicholas'

// 创建一个对象并保存在变量person 中,这个对象被传给setName() 方法,并被复制到参数obj 中。
// 在函数内部,obj 和person 都指向同一个对象。即使对象是按值传进函数,obj 也会通过引用访问对象。
// 当函数内部给obj 设置了name 属性时,函数外部的对象也会反映这个变化,因为obj 指向的对象保存在全局作用域的堆内存上。
// setName 函数内部将obj 重新定义为一个有着不同name 的新对象

function setName(obj) {
    obj.name = 'Nicholas'
    obj = new Object()
    obj.name = 'Greg'
}
let person = new Object()
setName(person)
console.log(person.name) // 'Nicholas'

// 当person 传入setName() 时,其name 属性被设置为"Nicholas"。
// 然后变量obj 被设置为一个新对象且name 属性被设置为"Greg"。
// 如果person 是按引用传递的,那person 应该自动将指针指向name 为"Greg" 的对象。
// 但再次访问person.name 时,它的值是"Nicholas",这表明函数中参数的值改变之后,原始的引用仍然没变。
// 当obj 在函数内部被重写时,它变成了一个指向本地的指针。而那个对象在函数执行结束后就被销毁了。 
// 基本数据类型
let foo = 1
const bar = value => {
    value = 2
    console.log(value)
}
bar(foo) // 2
console.log(foo) // 1
// 引用类型
let foo = {bar: 1}
const func = obj => {
    obj.bar = 2
    console.log(obj.bar)
}
func(foo) // 2
console.log(foo) // {bar: 2}


// 在函数体内直接修改对参数的引用
let foo = {bar: 1}
const func = obj => {
    obj = 2
    console.log(obj)
}
func(foo) // 2
console.log(foo) // {bar: 1}
  • 函数参数为基本数据类型时,函数体内复制了一份参数值,任何操作都不会影响到原参数的实际值
  • 函数参数是引用类型时,当在函数体内修改这个值的某个属性值时,将会对原来的参数进行修改
  • 函数参数是引用类型时,如果直接修改这个值的引用地址,则相当于在函数体内新创建了一个引用,任何操作都不会影响原参数的实际值

函数内部

ES5 中,函数内部存在两个特殊的对象:arguments 和this(两个隐式参数)。ES6 又新增了new.target 属性。

arguments

一个类数组对象,包含调用函数时传入的所有参数。这个对象只有以function 关键字定义函数时才会有。

  • arguments 对象可以使用中括号语法访问其中的元素:arguments[0]

  • arguments 对象可以跟命名参数(形参)一起使用

    function doAdd(num1, num2) {
        if (arguments.length === 1) {
            console.log(num1 + 10)
        } else if (arguments.length === 2) {
            console.log(arguments[0] + num2)
        }
    }
    
  • arguments 对象的值始终会与对应的命名参数同步:

    • arguments 对象的值和命名参数并不是访问同一个内存地址,它们在内存中是分开的,只不过会保持同步而已(在严格模式下,修改arguments 中的值不会影响对应的形参;非严格模式下对arguments 参数的修改会直接影响函数实参)
    • arguments 对象的长度(arguments 对象有length 属性,表示实参的确切个数。)是根据传入的参数(实参)个数,而非定义函数时给出的命名参数个数确定的。
  • 虽然主要用于包含函数参数,但arguments 对象有一个callee 属性,是一个指向arguments 对象所在函数的指针。

    arguments.callee已被弃用。

// 阶乘函数
function factorial(num) {
    if (num < 1) {
        return 1
    } else {
        return num * factorial(num - 1)
    }
}

// 重写
// 用arguments.callee 代替了之前硬编码的factorial
function factorial(num) {
    if (num < 1) {
        return 1
    } else {
        return num * arguments.callee(num - 1)
    }
}

// 无论函数叫什么名称,都可以引用正确的函数(无论通过什么变量调用这个函数都不会出问题)
// 因此在编写递归函数时,arguments.callee 是引用当前函数的首选
let trueFactorial = factorial
console.log(trueFactorial(5)) // 120

// 不过,在严格模式下运行的代码是不能访问arguments.callee 的,因为访问会出错
// 可以使用命名函数表达式(named function expression)达到目的
const factorial = (function f(num) {
    if (num < 1) {
        return 1
    } else {
        return num * f(num - 1)
    }
})

this

this 在标准函数和箭头函数中有不同的行为:

1、在标准函数中,this 引用的是把函数当成方法调用的上下文对象,这时候通常称其为this 值(在网页的全局上下文中调用函数时,非严格模式下this 指向window 对象;严格模式下指向undefined)。

2、箭头函数不会定义自己的this,它只会继承自己上一层作用域的this。

  • 对象中的箭头函数

    var obj = {
        i: 10,
        b: () => console.log(this.i, this),
        c: function() {
            console.log(this.i, this)
        }
    }
    obj.b()
    // 输出结果:undefined, Window{...}
    // 箭头函数b,没有上层作用域可以继承this,也就无法访问。
    
    obj.c()
    // 输出结果:10, Object {...} >>> {i: 10, b: ƒ, c: ƒ}
    
  • 正确使用箭头函数中的this

    // 实例1:
    function Person() {
        this.age = 0
    
        // 箭头函数作为 setInterval 的一个参数,继承了 this
        setInterval(() => {
            this.age++
            console.log('age:', this.age)
        }, 1000)
    }
    
    var p = new Person()
    
    // 实例2:
    // 箭头函数 student 继承了 showList 的 this
    let group = {
        title: "Our Group",
        students: ["John", "Pete", "Alice"],
        showList() {
            this.students.forEach(
                student => alert(this.title + ': ' + student)
            );
        }
    };
    
    group.showList()
    
  • 普通函数无法访问this

    // 普通函数中 this 等于 undefined ,所用 this.title 必然因为无法识别而报错
    let group = {
        title: 'Our Group',
        students: ['John', 'Pete', 'Alice'],
        showList() {
            this.students.forEach(function (student) {
                // Error: Cannot read property 'title' of undefined
                alert(this.title + ': ' + student)
            })
        }
    }
    
    group.showList()
    
function foo() {
    return () => {
        return () => {
            return () => {
                console.log('id', this.id)
            }
        }
    }
}

const f = foo.call({id: 1})

const t1 = f.call({id: 2})()() // id: 1
const t2 = f().call({id: 3})() // id: 1
const t3 = f()().call({id: 4}) // id: 1

// 上面的代码中只有一个this,就是函数foo 的this,所以t1 t2 t3都输出相同的结果。因为所有的内层函数都是箭头函数,都没有自己的this,它们的this 其实都是最外层foo 函数的this

在事件回调或定时回调中调用某个函数时,this 值指向的并非想要的对象。此时将回调函数写成箭头函数就可以解决问题。(箭头函数中的this 会保留定义该函数时的上下文)

caller

ES5 也会给函数对象上添加一个属性:caller。虽然ES3 中并没有定义,但所有浏览器除了早期版本的Opera 都支持这个属性。这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为null。

function outer() {
    inner()
}

function inner() {
    console.log(inner.caller)
    // 降低耦合度的写法:arguments.callee.caller
}

outer()

// 以上代码会显示outer() 函数的源代码

new.target

ES 中的函数始终可以作为构造函数实例化一个新对象,也可以作为普通函数被调用。ES6 新增了检测函数是否使用new 关键字调用的new.target 属性。如果函数是正常调用的,则new.target 的值是undefined,如果是使用new 关键字调用的,则new.target 将引用被调用的构造函数。

属性与方法

ES 中的函数是对象,因此有属性和方法。

length

保存函数定义的命名参数的个数。

prototype

保存引用类型所有实例的地方。toString()、valueOf() 等方法实际上都保存在ptototype 上,进而由所有实例共享。

call

call() 方法创建并返回一个新函数,并绑定在传入的对象上。

  • 第一个参数函数体内的this 指向,可不指定(在严格模式下是undefined,默认会绑定为全局对象)
  • 第二个参数,接收任意个参数。
function sum(num1, num2) {
    return num1 + num2
}

function callSum2(num1, num2) {
    return sum.call(this, num1, num2)
}
// 参数对应
function func(a, b, c) {
	console.log(a, b, c)
}

func.call(null, 1, 2, 3) // 1 2 3
func.call(null, [1, 2, 3]) // [1, 2, 3] undefined undefined
// 当调用 greet 方法的时候,该方法的this值会绑定到 obj 对象。
function greet() {
    var reply = [this.animal, 'typically sleep between', this.sleepDuration].join(' ')
    console.log(reply)
}

var obj = {
    animal: 'cats', sleepDuration: '12 and 16 hours'
}

greet.call(obj)  // cats typically sleep between 12 and 16 hours
使用场景

对象的继承

function superClass () {
    this.a = 1
    this.print = function () {
        console.log(this.a)
    }
}

function subClass () {
    // 执行函数,this 继承了 superClass 的 print 方法和 a 变量
    superClass.call(this)
    this.print()
}

subClass() // 1

类(伪)数组使用数组方法

// slice() - 浅拷贝,返回一个新的数组对象
let domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"))
手写call
Function.prototype.call = function (context = window) {
  if (typeof this !== 'function') {
    return new TypeError('error')
  }
  context.fn = this
  const args = [...arguments].slice(1)
  const res = context.fn(...args)
  delete context.fn
  return res
}

/* 以下是对实现的分析:
- 首先 context 为可选参数,如果不传的话默认上下文为 window
- 接下来给 context 创建一个 fn 属性,并将值设置为需要调用的函数
- 因为 call 可以传入多个参数作为调用函数的参数,所以需要将参数剥离出来
- 然后调用函数并将对象上的函数删除 */


Function.prototype.call2 = function(context, ...args) {
    context = (context === undefined || context === null) ? window : context
    context.__fn = this
    const res = context.__fn(...args)
    delete context.__fn
    return res
}

apply

apply() 方法接收两个参数:

  • 第一个参数函数体内的this 指向。如果不传,默认是全局对象window。
  • 一个参数数组或类数组,可以是Array 的实例,也可以是arguments 对象
 function sum(num1, num2) {
     return num1 + num2
 }
 ​
 function callSum1(num1, num2) {
     // this 值等于window,因为是在全局作用域中调用的
     return sum.apply(this, arguments) // 传入arguments 对象
 }
 ​
 function callSum2(num1, num2) {
     return sum.apply(this, [num1, num2]) // 传入数组
 }
 ​
 console.log(callSum1(10, 10)) // 20
 console.log(callSum2(10, 10)) // 20
 function func(a, b, c) {
     console.log(a, b, c)
 }
 ​
 func.apply(null, [1, 2, 3]) // 1 2 3
 func.apply(null, {
     0: 1,
     1: 2,
     2: 3,
     length: 3
 }) // 1 2 3
使用场景

获取数组中的最值

 // 重要的不是 this 的绑定对象,而是 apply 将 array 的数组拆解了作为参数给 Math.max
 let max = Math.max.apply(null, array)
 let min = Math.min.apply(null, array)

数组合并

 let arr1 = [1, 2, 3]
 let arr2 = [4, 5, 6]
     
 Array.prototype.push.apply(arr1, arr2)
 // 这里相当于把 arr2 作为 apply 的第二个参数,把 arr2 拆解了 -- arr1.push(4,5,6)
 // arr1 = [1, 2, 3, 4, 5, 6]
手写apply
/* 实现分析:
- 首先 context 为可选参数,如果不传的话默认上下文为 window
- 接下来给 context 创建一个 fn 属性,并将值设置为需要调用的函数
- 因为 apply 可以传入数组作为调用函数的参数,所以需要将参数剥离出来
- 然后调用函数并将对象上的函数删除 */

Function.prototype.apply2 = function(context, args) {
    context = (context === undefined || context === null) ? window : context
    context.__fn = this
    const res = context.__fn(...args)
    delete context.__fn
    return res
}
Function.prototype.applyFn = function (targetObject, argsArray) {
    if (typeof argsArray === undefined || argsArray === null) {
        argsArray = []
    }
    if (typeof targetObject === undefined || targetObject === null) {
        targetObject = window
    }
    targetObject = new Object(targetObject)
    
    // const targetFnKey = 'targetFnKey'
    const targetFnKey = Symbol()
    targetObject[targetFnKey] = this
    
    const result = targetObject[targetFnKey](...argsArray)
    delete targetObject[targetFnKey]
    return result
}

// 函数体内的this 指向了调用applyFn 的函数。为了将该函数体内的this 绑定在targetObject 上,采用隐式绑定的方法:
// targetObject[targetFnKey](...argsArray)
// 这里存在一个问题:如果targetObject 对象本身就存在targetFnKey 这样的属性,那么在使用applyFn 函数时,原有的targetFnKey 属性值就会被覆盖,之后被删除。
// 解决方案时使用ES6 Symbol() 来保证键的唯一性,或者使用Math.random() 实现独一无二的键。
const targetFnKey = Symbol()
call vs apply

Function.prototype.apply 和Function.prototype.call 的作用是一样的,区别在于传入参数的不同:

  • 第一个参数都是指定函数体内this 的指向
  • 第二个参数开始不同,apply 是传入带下标的集合,数组或类数组;call 从第二个开始传入的参数是不固定的,都会传给函数作为参数
  • call 比apply 的性能要好

bind

bind() 方法会创建一个新的函数实例,其this 值会被绑定到传给bind() 的对象。

window.color = 'red'
var o = {
    color: 'blue'
}
function sayColor() {
    console.log(this.color)
}
let objectSayColor = sayColor.bind(o)
objectSayColor() // blue
function func(a, b, c) {
    console.log(a, b, c)
}
const func1 = func.bind(null, 'D')
func1('A', 'B', 'C') // D A B
func1('B', 'C') // D B C
// 如果连续 bind() 两次,亦或者是连续 bind() 三次
const bar = function(){
    console.log(this.x)
}
const foo = {
    x: 3
}
const sed = {
    x: 4
}
const func = bar.bind(foo).bind(sed)
func() // ? => 3
 
const fiv = {
    x: 5
}
const func = bar.bind(foo).bind(sed).bind(fiv)
func() // ? => 3

// 两次都仍将输出3,而非期待中的 4 和 5。多次 bind() 是无效的。
手写bind
// 简单版
Function.prototype.bind2 = function(context, ...args) {
    context = (context === undefined || context === null) ? window : context
    let _this = this
    return function(...args2) {
        context.__fn = _this
        const res = context.__fn(...[...args, ...args2])
        delete context.__fn
        return res
    }
}


// 借助apply
Function.prototype.newBind = function (context) {
    let self = this // 调用 bind 的函数
    let args = [].slice.call(arguments, 1) // 调用 bind 时传入的参数
    return function() {
        // context - newBind 的第一个参数,即 this 要指向的对象
        self.apply(context, [...arguments,...args])
    }
}
// apply 模拟bind
// 预设参数传递功能(如bind 那样)
// 作为构造函数搭配new 关键字
Function.prototype.bind = Function.prototype.bind || function (context) {
    var _this = this
    var args = Array.prototype.slice.call(arguments, 1)
    var F = function() {} // 寄生组合继承
    F.prototype = this.prototype
    var bound = function () {
        var innerArgs = Array.prototype.slice.call(arguments)
        var finalArgs = args.concat(innerArgs)
        return _this.apply(this instanceof F ? this : context || this, finalArgs)
    }
    bound.prototype = new F()
    return bound
}

执行优先级

小括号(xxx) > 属性访问. > new foo() > foo()

function getName(){
    console.log(1)
}
function Foo() {
    this.getName = function () {
        console.log(2);
    }
    return this
}
Foo.getName = function () {
    console.log(3)
}
var a = new Foo.getName() // 属性.的优先级高于new foo(),该行等于new (Foo.getName)(),返回3

var b = new Foo().getName() // new foo()的优先级高于foo(),该行等于(new Foo()).getName(),返回2

var c = new new Foo().getName() // new foo()优先级低于属性.,该行等于new (new Foo().getName)(),相当于new一个new foo()的getName属性函数,返回2

【参考资料】

《JavaScript 高级程序设计》第4 版 章节10-函数

《JavaScript 语言精髓与编程实践》第3版 章节5

《JS 忍者秘籍》第2版 章节3、4、5

《JS 悟道》章节12