前言:前端现在越来越卷,找工作拿高薪更是不易,因为想要在这个特别卷的时代,我们要想进大厂,拿高薪,首先最重要的就是,知识点得掌握扎实,做到举一反三。
因此
this、call、apply、bind
还不是很清楚的,文章干货很多,请先收藏,慢慢阅读,让你直接 拿捏 这些重要经典知识点。
1、this的指向
this的指向是面试中必考且比较难得一个点,本文以最简单最好理解的方式来解读this
指向的问题。
在ES5
中,this的指向是原则:永远指向最后调用它的那个对象
。 这一点非常关键:理解this,必须牢牢记住这句话。
看看一些例子:
例1:
var name = 'windowName';
function example(){
var name = 'object';
console.log(this.name); //windowName
}
example()
这里为什么输出的是 window 而不是object ,原因在于: 函数 example
在调用时,直接调用 example()
,前面没有对象,相对于这样调用 window.example()
;又:this 永远指向最后调用它的那个对象 ,所以输出 window。
例2:
var name = "windowName";
var o = {
name : "object",
fn : function(){
console.log(this.name); //object
}
}
o.fn()
//window.a.fn()
在这里打印的是 object
,因为函数 是 对象 o
调用的。
这里换成 window.a.fn()
也是同样的结果,因为最后还是 对象 a
调用的,印证了那句话 :**this永远指向它最后调用的对象 **。
例3:面试中很坑的一个例子。
var name = "windowName";
var o = {
name : "object",
fn : function(){
console.log(this.name); //windowName
}
}
const f = o .fn;
f()
这里就让人很懵,大家肯定都很好奇原因,首先要明白 const f = o .fn;
这一步是声明变量f
并赋值的操作,在这里只是将对象 o 的方法 fn
赋值给 f 并没有调用,而最后调用f()
,(实际是调用o .fn()
) ,是在window对象下调用的,因此this 指向的是 window , 所有输出的是 :windowName
。
2、改变this的指向?
通常改变this的方法有一下几种方法:
- 使用call、apply、bind。
- ES 6 中的箭头函数。
- 函数内部一般使用
that = this
。 new
实例化一个对象。- 隐式绑定
2.1 使用 apply、call、bind
1、使用call
var person = {
name:'王涵宇'
}
var o = {
name : "object",
fn : function(){
console.log(this.name); //王涵宇
}
}
o.fn.call(person)
2、使用apply
/*
以call实例为例
*/
o.fn.apply(person) //王涵宇
3、使用bind
/*
以call实例为例
*/
o.fn.bind(person)() // 王涵宇
//注意这里的调用方式与上面的不同
2.2 使用 ES 6 中的箭头函数。
使用箭头函数:
var name = "windowName";
var o = {
name : "object",
fn1 : function(){
console.log(this.name); //object
},
fn2 : function(){
setTimeout(()=>{
this.fn1()
},1000)
}
}
o .fn2();
不使用箭头函数:
var name = "windowName";
var o = {
name : "object",
fn1 : function(){
console.log(this.name); //Uncaught TypeError: this.fn1 is not a function
},
fn2 : function(){
setTimeout(function(){
this.fn1()
},1000)
}
}
o .fn2();
会报错,因为 最后是 window对象
调用 setTimeOut
,而 window对象上没有 fn1
这个方法。
2.3 函数内部使用 that = this
。
var name = "windowName";
var o = {
name : "object",
fn1 : function(){
console.log(this.name); // object
},
fn2 : function(){
let that = this; //保存this
setTimeout(()=>{
that.fn1()
},1000)
}
}
o .fn2();
2.4 new
实例化一个对象
var name = "windowName";
function getName(){
this.name = '王涵宇';
console.log(this); //getName {name: '王涵宇'} this指向这个函数(对象)
}
new getName()
2.5 隐式绑定
说白了:就是谁调用指向谁。
const obj = {
name :'王涵宇',
getName : function(){
console.log(this);
console.log(this.name) //王涵宇
}
}
obj.getName()
3、实现call、apply、bind
1、实现call
1、这三种方法都在函数的 原型上 接受两个参数
Function.prototype.myCall = function(obj,...args){
//这里默认不传obj。就是给 window
obj = obj || window;
args = args ? args : [];
//给obj添加一个独一无二的属性,防止与调用函数中有相同的属性变量
const fn = Symbol();
obj[fn] = this;
//通过隐式绑定的方式调用函数
//在这里用一个变量保存调用的结果,完了后删除这个属性,防止造成影响
const res = obj[fn](...args);
delete obj[fn](...args);
return res;
}
const obj1 = {
name:'ustinian',
getName:function(age){
console.log(`${this.name}今年${age}了!`) //王涵宇今年22了!
}
}
const obj2 = {
name:'王涵宇',
}
obj1.getName.myCall(obj2,22)
2、实现apply
Function.prototype.myApply = function(obj,args){
//这里默认不传obj。就是给 window
obj = obj || window;
args = args ? args : [];
//给obj添加一个独一无二的属性,防止与调用函数中变量属性重名
const fn = Symbol();
obj[fn] = this;
//隐式绑定的方式调用函数
const res = obj[fn](args);
delete obj[fn](args);
return res;
}
3、实现bind
bind
方法的第一个参数this
是可以不传的,分2种情况
- 直接调用bind之后的方法
var f = function () { console.log('不传默认为'+this) };f.bind()()
// 不传默认为 Window
所以直接调用绑定方法时候
apply(that,
建议改为 apply(that||window,
,其实不改也可以,因为不传默认指向window
- 使用
new
实例化被绑定的方法
重点在于弄清楚标准的bind方法在new的时候做的事情,就可以清晰的实现
这里我们需要看看 new
这个方法做了哪些操作 比如说 var a = new b()
- 创建一个空对象
a = {}
,并且this
变量引用指向到这个空对象a
- 继承被实例化函数的原型 :
a.__proto__ = b.prototype
- 被实例化方法
b
的this
对象的属性和方法将被加入到这个新的this
引用的对象中:b
的属性和方法被加入的a
里面 - 新创建的对象由
this
所引用 :b.call(a)
// 原生JS 的实现
Function.prototype._bind = function(context) {
if (typeof this !== 'function') {
throw new Error('error callback');
}
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
// 注意:这里如果直接将 fnBind.prototype = this.prototype
// ,我们直接修改 fnBind.prototype 的时候,也会直接修改绑定函数的 prototype。
// 解决方案:这个时候,我们可以通过一个空函数来进行中转
var fnOP = function() {};
var fnBind = function() {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fnOP
? this
: context
, args.concat(bindArgs));
}
// 通过间接地去修改这个中间函数的原型对象,new 的方式来避免直接修改原型上面的属性和对象参数信息
fnOP.prototype = this.prototype;
// 这里不就是原型链继承的实现吗?
fnBind.prototype = new fnOP();
return fnBind;
}
// ES6的写法一
Function.prototype._bind = function(context, ...args) {
let self = this;
return function() {
self.apply(context, args.concat(Array.prototype.slice.call(arguments)));
}
}
// ES6的写法二
Function.prototype._bind = function(context) {
context = context || window;
let args = Array.prototype.slice.call(arguments, 1);
let self = this;
// 构建一个中间(空)的函数,维护原型之间的关系
let Empty = function(){};
let F = function() {
self.apply(this instanceof Empty
? this
: context
, args.concat(Array.prototype.slice.call(arguments)))
}
// 通过中间函数维护原型关系
Empty .prototype = this.prototype;
F.prototype = new Empty ();
return F;
}