题外篇
如何改变this得指向,常见的四种操作如下
- 使用
call、apply、bind - 在执行函数内部使用
let that = this es6中使用箭头函数- 对象实例化
new操作
关于this的指向
在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。如果函数独立调用,那么该函数内部的this,则指向undefined。但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。
参考来自 波波老师
call
根据 MDN 的解释:
call()方法调用一个函数, 其具有一个指定的this值和分别地提供的参数(参数的列表)。
语法:
fun.call(thisArg, arg1, arg2, ...)
thisArg:fun函数运行时指定得this值
this得值可能有如下几种可能:
- 在非严格模式下,传
null,undefinedor 不传,this默认指向window对象 - 传其他函数的函数名称,如
fn,this指向fn函数 - 传其他对象,
this指向这个对象
看个例子
let obj = {
val:'call'
}
function fn () {
console.log(this.val,'testCall');
}
fn.call(obj) //'call','testCall'
模拟开搞
思考一下上面例子的代码执行过程
call()在执行过程中,我们想象一下它大概会经历一些几个阶段
- 将
fn方法复制到obj对象中 - 改变
fn函数的this指向 - 将
fn函数执行 - 把
fn从obj对象删除
分析:
那么我们在模拟代码的场景下
fn.call(obj)的执行过程可以想象成如下步骤:
1、将fn复制到obj对象中
obj = {
val:'call',
fn:function(){
console.log(this.val,'testCall')
}
2、 执行fn(),下面的调用方式也就改变了 fu 函数的 this 指向
obj.fn()
3、删除fn这个key
delete obj.fn
模拟开始
Function.prototype.call2 = function(args){
//此时的args就是 上面的obj
// 根据Function的原型链继承,此时的this是fn
args.fn = this;
//第二步 调用执行fn()
args.fn();
//第三步 删除方法
delete args.fn
}
//测试下
let obj = {
val:'call2'
}
function fn () {
console.log(this.val,'testCall2');
}
fn.call2(obj) //'call2','testCall2'
Nice Amazing 搞定了 好像也不复杂,等等
仔细阅读
MDN文档发现,call可以接受多个参数,那么在我们需要完善一下这个功能
强化版
栗子
let obj = {
val:'call'
}
function fn (name) {
console.log(this.val,name);
}
fn.call(obj,'alan') //'call','alan'
分析:
- 跟第一阶段相比就是多了一个传参,有疑惑的地方,可能不知道穿几个参数,不慌,可以从
Arguments中获取第二个开始到最后结束的参数就行了
强化开始
// 强化版
Function.prototype.call2 = function(...args) {
//利用es6的 rest 来获取函数的传参,以及传入thisArg;
let [thisArg,...arr] = args ;
// 获取调用的函数方法
thisArg.fn = this;
// 用解构执行函数
thisArg.fn(...arr)
//删除
delete thisArg.fn
}
let obj = {
val:'call'
}
function fn (name) {
console.log(this.val,name);
}
fn.call2(obj,'alan') //'call','alan'
解释:
es6的 rest (形式为...变量名),这样可以得到一个数组,即args此时为数组,那么上文中的thisArg就是传递的第一个参数。fn(..arr)使用了es6spread ,他就好比是reset的逆运算,这样操作以后不管传递了几个参数都可以正常处理
也可以使用
es5的slice进行参数切割
到这个时候感觉基本完成了模拟手写得功能
不过文章开头介绍过,如果在严格模式下,传
null,undefinedor 不传,thisArg默认指向window对象,还有一种场景如果fn方法有返回值的情况。
处理 传入 null 、undefined
栗子 1
var val = 'call'
function fn () {
console.log(this.val);
}
fn.call() //'call'
fn.call(null);//'call'
fn.call(undefind);//'call'
分析:
如果不传值或传null等值,处理起来不算麻烦,稍微在我们原来的版本上做一些修改就好,看如下代码
// 最终版本
Function.prototype.call2 = function(...args) {
let thisArg,arr = [];
if(args.length === 0 || !args[0]){ // 判断下 特殊情况 处理下全局对象
thisArg = typeof window !== 'undefined' ? window : global //兼容下 node环境
} else{
//利用es6的解构来获取函数的传参,以及传入thisArg;
[thisArg,...arr] = args ;
}
// 获取调用的函数方法
thisArg.fn = this;
// 用解构执行函数
thisArg.fn(...arr)
//删除
delete thisArg.fn
}
fn.call2() //'call'
fn.call2(null);//'call'
fn.call2(undefind);//'call'
处理函数有返回值
栗子2
let obj = {
val:'call'
}
function fn (name) {
console.log(this.val,name);
return {
val:this.val,
name:name
}
}
fn.call(obj,'alan') //'call','alan'
//
{
val:'call',
name:'alan'
}
强强化终极版本
// 3.2
Function.prototype.call2 = function(...args) {
let thisArg,arr = [];
if(args.length === 0 || !args[0]){
thisArg = typeof window !== 'undefined' ? window : global //兼容下 node环境
} else{
//利用es6的解构来获取函数的传参,以及传入thisArg;
[thisArg,...arr] = args ;
}
// 获取调用的函数方法
thisArg.fn = this;
// 用解构执行函数
let result = thisArg.fn(...arr)
//删除
delete thisArg.fn
return result
}
let obj = {
val:'call'
}
function fn (name) {
console.log(this.val,name);
return {
val:this.val,
name:name
}
}
fn.call2(obj,'alan') //'call','alan'
//
{
val:'call',
name:'alan'
}
apply
apply的实现方式跟call基本相似,就是在传参上,apply接受的是数组,直接就贴一下代码
Function.prototype.apply2 = function(thisArg,arr) {
if(!thisArg){
thisArg = typeof window !== 'undefined' ? window : global //兼容下 node环境
}
// 获取调用的函数方法
thisArg.fn = this;
// 用解构执行函数
let result = thisArg.fn(...arr)
//删除
delete thisArg.fn
return result
}
let obj = {
val:'apply'
}
function fn (name) {
console.log(this.val,name);
return {
val:this.val,
name:name
}
}
fn.apply2(obj,['alan']) //'apply','alan'
bind
根据 MDN 的解释:
bind()方法创建一个新的函数,在调用时设置this关键字为提供的值。将给定参数列表作为原函数的参数序列的前若干项。
语法:
fun.bind(thisArg,arg1,arg2......)
-
bind()方法会创建一个新的函数,一般叫绑定函数 -
可以接受参数,这个地方注意,它可以在
bind的时候接受参数,同时bind()返回的新函数也可以接受参数
栗子
var obj = {
val: 'bind'
};
function fn() {
console.log(this.val);
}
// 返回了一个函数
var bindObj = fn.bind(obj);
bindObj(); // bind
模拟第一版
照旧,暂时不考虑传参
分析:
bindObj()的执行结果跟使用call一样的,不同的是它需要调用返回的方法bindObj
琢磨上述代码执行过程,这个时候我们对比一下call的模拟来看
bindObj像是call模拟过程中的fn,而后bindObj()就像是fn()bind返回的函数,我们可以想象成call()调用只有返回的函数而不会执行,只是apply(),call()是立即执行,而bind需要再次调用执行
模拟开始
Function.prototype.bind2 = function (args) {
//通过this拿到调用方法
let that = this;
//使用一个闭包来存储call方法的结果
return function () {
return that.call(args);
}
}
var obj = {
val: 'bind'
};
function fn() {
console.log(this.val);
}
// 返回了一个函数
var bindObj = fn.bind2(obj);
bindObj(); // bind
模拟第二版
考虑下传参的场景,开头介绍过,传参有两种场景
栗子
let obj = {
val:'bind'
};
function fn(name,sex){
let o = {
val:this.val,
name:name,
sex:sex
}
console.log(o)
}
let bindObj = fn.bind(obj,'alan');
bindObj('man'); //{ val: 'bind', name: 'alan', sex: 'man' }
栗子分析:
- 首先
bind的时候接受了一个参数name,同时返回了一个函数 - 执行放回的函数的时候传入了第二个参数
sex
模拟分析:
- 首先考虑
bind方法传参的场景,我们可以借用之前在call函数中的方法,使用es6 rest的方法。获取从第二个开始到结束的所有参数 - 考虑
bind返回的函数传参,可以在写的时候,将bind传参跟后续的传参合并
模拟开发
// 2.1
Function.prototype.bind2 = function (args) {
//通过this拿到调用方法
let that = this;
// 获取bind2函数从第二个参数到最后一个参数
let allArgs = Array.prototype.slice.call(arguments, 1);
return function () {
// 这个时候的arguments是指bind返回的函数传入的参数
var bindArgs = Array.prototype.slice.call(arguments);
return that.apply(args, allArgs.concat(bindArgs));
}
}
//2.2 es6实现
Function.prototype.bind2 = function (...args) {
//利用es6的 rest 来获取函数的传参,以及传入thisArg;allArgs就是第二个参数到最后一个参数的数组
let [thisArg,...allArgs] = args ;
let that = this;
return function (...bindArgs) {
return that.apply(thisArg, allArgs.concat(bindArgs));
}
}
let obj = {
val:'bind'
};
function fn(name,sex){
let o = {
val:this.val,
name:name,
sex:sex
}
console.log(o)
}
let bindObj = fn.bind2(obj,'alan');
bindObj('man'); //{ val: 'bind', name: 'alan', sex: 'man' }
说明
Array.prototype.slice.call(arguments)是如何将arguments转换成数组的,首先调用call之后,this就指向了arguments,或许我们可以假象一下slice的内部实现是:创建一个新的数组,然后循环遍历this,将this的没一个值赋值给新的数组然后返回新数组。
结束语
大佬如果看到文中如有错误的地方欢迎指出,我及时修正。