一、call
作用:改变 this 指向。
用法:
function person(age, hobby){
console.log( '姓名:', this.name ); // 姓名: 翠花
console.log( '年龄:', age ); // 年龄: 18
console.log( '爱好:', hobby ); // 爱好: 坤坤
}
const obj = {
name: '翠花',
}
person.call( obj, 18, '坤坤' );
原理:
1. 在对象的属性中,增加一个方法:
function person(){
console.log( '姓名:', this.name ); // 姓名: 翠花
console.log( '年龄:', age ); // 年龄: 18
console.log( '爱好:', hobby ); // 爱好: 坤坤
}
const obj = {
name: '翠花',
fun: person,
}
person.call( obj, 18, '坤坤' ); // → obj.fun()
2. 调用完方法后,再将这个方法清除掉
完整代码:
Function.prototype.myCall = function( obj ){
obj = obj || window;
obj.fun = this;
const res = obj.fun( ...([].slice.call(arguments, 1)) );
delete obj.fun;
return res;
}
// --------------- 测试 CODE ------------------
function person(age, hobby){
console.log( '姓名:', this.name ); // 姓名: 翠花
console.log( '年龄:', age ); // 年龄: 18
console.log( '爱好:', hobby ); // 爱好: 坤坤
}
const obj = {
name: '翠花'
}
person.myCall( obj, 18, '坤坤' );
// --------------- 测试 CODE ------------------
问题1:obj = obj || window; 的作用?
myCall 的第一个参数为 null 时,需要将 obj 指向 window
function fun(){
console.log('this: ', this); // this: Window {0: Window, 1: Window, window: Window, self: Window, document: document, name: '', location: Location, …}
}
fun.call(null, 1, 2)
问题2:myCall 函数中的 this 指向?
函数的调用者 person。person 调用了 myCall,所以 this 就指向 person
问题3:为什么需要用 “[ ].clice.call(arguments, 1)” 获取 arguments 中第 2~n 个参数?
function fun(){
console.log(arguments.slice(1)); // Uncaught TypeError: arguments.slice is not a function
}
fun('1', '2');
1. arguments 是一个伪数组,并非真正的数组,所以在 arguments 的原型上不存在 slice 方法,arguments 并不能直接调用 slice。
借助空数组调用 slice 方法,将 this 指向 arguments,就可以实现让一个伪数组调用数组的 slice 方法了。
2. 调用 call 传入的参数,除了第一个,剩下的都要传递到函数中去。
问题4:第五行代码,将函数执行结果记录起来并返回的作用?
调用 myCall 的函数可能有返回值
function person( age, hobby ){
return {
name: this.name,
age: age,
hobby: hobby,
}
}
const obj = {
name: '翠花',
}
let objInfo = person.myCall( obj, 18, '坤坤' );
console.log( objInfo ); // {name: '翠花', age: 18, hobby: '坤坤'}
如果只在 myCall 中执行了 person 函数,而不将函数返回值返回出去,调用 myCall 得到的结果就会是 undefined
二、apply
作用:和 call 一样,改变 this 指向。
用法:参数和 call 有一些区别,call 传递给函数的参数是一个平铺结构,而 apply 保存在一个数组中:
function person(age, hobby){
console.log( '姓名:', this.name ); // 姓名: 翠花
console.log( '年龄:', age ); // 年龄: 18
console.log( '爱好:', hobby ); // 爱好: 坤坤
}
const obj = {
name: '翠花'
}
person.call( obj, 18, '坤坤' );
person.apply( obj, [18, '坤坤'] );
完整代码:
Function.prototype.myApply = function( obj, arg ){
obj.fun = this || window;
const res = obj.fun( ...(arg || []) );
delete obj.p;
return res;
}
总结
- 调用 call、apply 在本质上,都相当于将函数绑定到对象内,执行函数,然后删除对象中的函数,实现 this 的调整
- 调用 myCall 时如果传入第一个参数为 null,指定 this 为 window
- 最后,一定要将 person 的执行结果返回
三、“函数柯里化” + “圣杯模式继承” 实现 bind
作用:返回一个改变了 this,但其它均和原函数基本一致的函数
用法:
function person(age, hobby){
console.log( '姓名:', this.name ); // 姓名: 翠花
console.log( '年龄:', age ); // 年龄: 18
console.log( '爱好:', hobby ); // 爱好: 坤坤
}
const obj = {
name: '翠花'
}
const newPerson = person.bind( obj, 18 );
newPerson('坤坤'); // 可以像调用 person 一样调用 newPerson
1. “基本用法”实现
Function.prototype.myBind = function( obj ){
obj = obj || window;
const fun = this;
const args = [].slice.call(arguments, 1);
return function (){
return fun.apply( obj, args.concat( ...arguments) ); // 将两个函数的参数合并到一个数组中
}
}
问题:args.concat( ...arguments) 的作用?
函数柯里化(Currying)。把接受多个参数的函数变换成接收一个单一参数。
调用 bind 时,传递的第 2~n 个参数,与被返回函数的参数,一并传入到 person 中去
const obj = {
name: '翠花'
}
// 下面三种用法效果是相同的
const newPerson1 = person.bind( obj );
newPerson1(18, '坤坤');
const newPerson2 = person.bind( obj, 18 );
newPerson2('坤坤');
const newPerson3 = person.bind( obj, 18, '坤坤' );
newPerson3();
2. “实例化用法” 实现
bind 返回的函数,可以作为构造函数,通过 new 来实例化对象
但这样使用 myBind 时,this 指向及原型链会存在一些问题:
// myBind 校验代码:
function person(){
console.log('this: ', this);
console.log('this instanceof person: ', this instanceof person);
console.log('this.name: ', this.name);
}
const obj = {
name: '翠花'
}
const BindPerson = person.bind( obj );
let iKun1 = new BindPerson();
// this: person {}
// this instanceof person: true
// this.name: undefined
const MyBindPerson = person.myBind( obj );
let iKun2 = new MyBindPerson();
// this: {name: '翠花'}
// this instanceof person: false
// this.name: '翠花'
可以很明显的看到,构造函数用法中,myBind 内 this 的指向是错误的。
思路是借助:不同的用法,被返回函数内的 this 指向是不同的
Function.prototype.myBind = function(){
const myBindReturnFun = function (){
console.log('inner-this: ', this);
}
return myBindReturnFun;
}
function person(){}
const obj = {
name: '翠花'
}
const MyBindPerson = person.myBind( obj );
const iKun1 = MyBindPerson(); // inner-this: Window {0: Window, 1: Window, window: Window, self: Window, document: document, name: '', location: Location, …}
const iKun2 = new MyBindPerson(); // inner-this: myBindReturnFun {} (由 MyBindPerson 构造出来的实例)
借助这一点,就可以做区分,针对性地修改 this 指向
Function.prototype.myBind = function( obj ){
const fun = this;
const args = [].slice.call( arguments, 1 );
const myBindReturnFun = function (){
const twoArgs = args.concat( ...arguments );
// ---------------- 针对不同的用法,修改不同的 this -----------------
fun.apply( this instanceof myBindReturnFun ? this : obj, twoArgs );
// --------------------------------------------------------------
}
return myBindReturnFun;
}
再次运行上面的校验代码
const BindPerson = person.bind( obj );
let iKun1 = new BindPerson();
// this: person {}
// this instanceof person: true
// this.name: undefined
const MyBindPerson = person.myBind( obj );
let iKun2 = new MyBindPerson();
// this: myBindReturnFun {}
// this instanceof person: false
// this.name: undefined
this 已经指向 myBindReturnFun 的实例化对象(myBindReturnFun 和 person 是等效的)
但是原型链的问题依然没有被解决
解决这个问题也很简单,调整 myBindReturnFun 的原型链即可:
Function.prototype.myBind = function( obj ){
const fun = this;
const args = [].slice.call( arguments, 1 );
// 创建一个空函数
var emptyFun = function () {};
const myBindReturnFun = function (){
const twoArgs = args.concat( ...arguments );
fun.apply( this instanceof myBindReturnFun ? this : obj, twoArgs );
}
// 圣杯继承:调整后的原型链 myBindReturnFun.prototype.__proto__ → emptyFun.prototype = this.prototype
emptyFun.prototype = this.prototype;
myBindReturnFun.prototype = new emptyFun();
return myBindReturnFun;
}