原生/内建函数
常用的原生函数:
- 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
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
- 执行Foo 函数的静态方法,打印出2。
- 执行getName,当前getName是打印出4 的那个函数。
- 执行Foo 函数,修改了全局变量getName,赋值成了打印1 的函数,然后返回this,因为是在全局环境下执行,所以this 指向window,因为getName 已经被修改了,所以打印出1。
- 因为getName 没有被重新赋值,所以再执行仍然打印出1。
- new 操作符是用来调用函数的,所以
new Foo.getName()
相当于new (Foo.getName)()
,所以new 的是Foo 的静态方法getName,打印出2。 - 因为点运算符(.)的优先级和new 是一样高的,所以从左往右执行,相当于
(new Foo()).getName()
,对Foo使用new 调用会返回一个新创建的对象,然后执行该对象的getName 方法,该对象本身并没有该方法,所以会从Foo 的原型对象上查找,找到了,所以打印出3。 - 和上题一样,点运算符(.)的优先级和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}`
}
- 参数按顺序初始化,所以后定义默认值的参数可以引用先定义的参数。
- 参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的,否则会抛出错误。
剩余参数
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