涨薪必备Javascript,快点放进小口袋!
摘要:
嗨,新一年的招聘季,你找到更好的工作了吗?小姐姐最近刚换的工作,来总结下面试必备小技能,从this来看看javascript,让我们更深入的了解它。
前言
在JavaScript中,被吐槽最多的this,原型,继承,闭包等这些概念。接下来这篇文章会把我自己对于JavaScript中这些点通过this指向做个总结并分享给大家,希望可以帮助大家更好的了解这些所谓的难点。
一、this
this是什么?this的常见使用场景呢?
- 普通调用,this指向为调用者
- call/apply调用,this指向为当前thisArg参数
- ES6新增的箭头函数,this指向为当前函数的this指向
这个怎么理解呢?接下来我会一一做解析。
1、普通调用
通俗理解一下,就是谁调用,则this便指向谁。这里又大致分为几种情况,分别为
1.1、对象方法的调用
即某方法为某对象上的一个属性的属性,正常情况当改方法被调用的时候,this的指向则是挂载该方法的对象。废话不多说,直接看代码可能会更好的理解。
var obj={
a:'this is obj',
test:function(){
console.log(this.a);
}
}
obj.test(); //this-->obj
1.2、“单纯”函数调用
即该函数为自己独立的函数,而不是挂载到对象上的属性(window除外),也不会被当成构造函数来使用,而仅仅是当成函数来使用,此时的this指向则是window对象。例子如下
var a='this is window';
function test(){
console.log(this.a);
}
test(); //this-->window
这个我们来理解一下,其实也很简单,我们都知道,window对象是全局对象。其实整个代码块等同于
window.a='this is window';
window.test=function test(){
console.log(this.a);
//此时window为调用者,即this会指向window
}
window.test();
1.3、构造函数调用
即该函数被当成构造函数来调用,此时的this指向该构造器函数的实例对象。
我们来看一个例子,先上一个属于第二种情况的例子
function test(){
this.a='this is test';
console.log(this.a);
console.log(this);
}
test();
//this is test this---->window
//Window {}
按照上面的来理解,此时的this的确指向window对象,但是如果我换种形式,将其换成构造函数来调用呢,结果又会如何呢,直接上代码
function Test(){
this.a='this is test';
console.log(this.a);
console.log(this);
}
var test= new Test();
//this is test this--->new Test()
//Test {a:'this is test'}
OK,好像的确没有问题了,此时的this的确指向了该构造函数的实例对象。具体这里的一些解释后面我会在原型链继承里面详细讲解。
2、call/apply调用
2.1、call调用
call方法形式,fun.call(thisArg[, arg1[, arg2[, …]]])
- thisArg,当前this指向
- arg1[, arg2[, …]],指定的参数列表
详细介绍请猛戳MDN
示例代码如下
function Test () {
this.a = 'this is test';
console.log(this.a);
console.log(this);
}
function Test2 () {
Test.call(this)
}
var test = new Test2();
// this is test
// Test2 {a: 'this is test'}
2.2、apply调用
和call类似,唯一的一个明显区别就是call参数为多个,apply参数则为两个,第二个参数为数组或类数组形式, fun.apply(thisArg, [argsArray])
- thisArg,当前this指向
- 一个数组或者类数组对象,其中的数组元素将作为单独的参数传给fun函数
详细介绍请猛戳MDN
但是终究apply里面的数组参数会转变为call方法的参数形式,然后去走下面的步骤,这也是为什么call执行速度比apply快。这边详情有篇文章有介绍,点击链接。
另外,提及到call/apply,怎么能不提及一下bind呢,bind里面的this指向,会永远指向bind到的当前的thisArg,即context上下文环境参数不可重写。这也是为什么a.bind(b).call(c),最终的this指向会是b的原因。至于为什么,其实就是bind实现实际上是通过闭包,并且配合call/apply进行实现的。具体的请参考bind MDN里面的用法及 Polyfill实现。
3、箭头函数
首先需要介绍的一点就是,在箭头函数本身,它是没有绑定本身的this的,它的this指向为当前函数的this指向。怎么理解呢,直接上个代码看下
function test(){
(()=>{
console.log(this);
})()
}
test.call({a:'this is thisArg'}); //Object {a:'this is thisArg'}
这样看联想上面的call/apply调用的理解,好像是没有问题了,那如果我设置一个定时器呢,会不是this指向会变成Window全局对象呢?答案肯定是不会的,因为箭头函数里面的this特殊性,它依旧会指向当前函数的this指向。不多BB,直接看代码
function test(){
setTimeout(()=>{
console.log(this);
},0)
}
test.call({a:'this is obj'});
//Object {a:'this is obj'};
当然普通函数使用setTimeout的话会让this指向指向Window对象的。demo代码如下
function test(){
setTimeout(function(){
console.log(this);
},0)
}
test.call({a:'this is obj'}); //this---->Window
这里可能会牵扯到setTimeout的一些点了,具体这里我就不讲了,想深入了解的猛戳这里
箭头函数里面还有一些特殊的点,这里由于只提及this这一个点,其他比如不绑定arguments,super(ES6),抑或 new.target(ES6),他们都和this一样,他会找寻到当前函数的arguments等。
关于箭头函数里面的this这里也有详细的介绍,想深入了解的可以自行阅读
二、原型/原型链
其实我们一看到原型/原型链都能和继承联想到一起,我们这里就把两块先拆开来讲解,这里我们就先单独把原型/原型链拎出来。首先我们自己问一下自己,什么是原型?什么是原型链?
- 原型:即每个function函数都有的一个prototype属性。
- 原型链:每个对象和原型都有原型,对象的原型指向原型对象,而父的原型又指向爷爷辈,直到最上层祖先Object.prototype.proto=null;这种原型层层连接起来的就构成了原型链。
好像说的有点绕,其实一张图可以解释一切
那么这个东西有怎么和指向这个概念去联系上呢?其实这里需要提及到的一个点,也是上面截图中存在的一个点,就是__proto__,我喜欢把其称为原型指针。终归到头,prototype只不过是一个属性而已,它指的是原型这个壳子,最后能做原型链继承的还是通过__proto__这个原型指针来完成的。我们看到的所谓的继承只不过是将需要继承的属性挂载到继承者的prototype属性上面去的,实际在找寻继承的属性的时候,会通过__proto__原型指针一层一层往上找,即会去找__proto__原型指针它的一个指向。看个demo
funtcion Test(){
this.a='this is Test';
}
Test.prototype={
b:function(){
console.log("this is Test's prototype");
}
}
function Test2(){
this.a='this is Test2';
}
Test2.prototype=new Test(); //此处Test2继承Test,用子类的原型指向父类的实例
var test=new Test2();
test.b();
console.log(test.prototype);
console.log(test);
其执行结果如下
总结:原型即prototype,它只是所有function上的一个属性而已,真正的“大佬”是__proto__,“大佬”指向谁,谁才能有言语权(当然可能因为“大佬”过于霸道,所以在ECMA-262之后才被Standard化)。
三、继承
这里我们通过指向这个概念来重新理解一下继承。这里咱就谈两个万变不离其宗的继承方式,一个是构造函数继承,一个是原型链继承。
1、构造函数继承
其实就是上面提及到的通过call/apply调用,将this指向变成thisArg,具体看上面的解释,这里直接上代码
function Test(){
this.a='this is test';
console.log(this.a);
console.log(this);
}
function Test2(){
Test.apply(this);
}
var test=new Test2();
//this is test
//Test2 {a:'this is test'}
2、原型链继承
一般情况,我们做原型链继承,会通过子类prototype属性等于(指向)父类的实例。即
Child.prototype = new Parent();
那么这样的做法具体是怎么实现原型链继承的呢?
首先在讲解继承前,我们需要get到一个点,那就是对象{ }它内部拥有的一些属性,这里直接看张图
如上图所示,我们看到对象{ }它本身拥有的属性就是上面我们提及到的__proto__原型指针以及一些方法。
接下来我先说一下new关键字具体做的一件事情。其过程大致分为三步,如下
var obj = {}; // 初始化一个对象obj。
obj.__proto__ = Parent.prototype; // 将obj的__proto__原型指针指向父类Parent的prototype属性
Parent.call(obj); // 初始化Parent构造函数
从这里我们看出来,相信大家也能理解为什么我在上面说__proto__才是真正的“大佬”。
这里我额外提一件我们经常干的“高端”的事情,那就是通过原型prototype做monkey patch。即我想在继承父类方法的同时,完成自己独立的一些操作。具体代码如下
function Parent(){
this.a='this is Parent';
}
Parent.prototype={
b:function(){
console.log(this.a);
}
};
function Child(){
this.a='this is Child';
}
Child.prototype={
b:function(){
console.log('monkey patch');
Parent.prototype.b.apply(this);
}
}
var child=new Child();
child.b(); //monkey patch //this is Child