摘要: 前言 开写前你们先来理解一下指向:指向,即目标方向、所对的方位。 不少人刚刚接触前端甚至一些“老”前端都常常会在JavaScript中所谓的难点,如this,原型,继承,闭包等这些概念中迷失了自我。html
开写前你们先来理解一下指向:指向,即目标方向、所对的方位。前端
不少人刚刚接触前端甚至一些“老”前端都常常会在JavaScript中所谓的难点,如this,原型,继承,闭包等这些概念中迷失了自我。接下来这篇文章会把我本身对于JavaScript中这些点经过指向的概念作个总结并分享给你们,但愿能够帮助你们更好的了解这些所谓的难点。数组
this是什么?其实它自己就是一种指向。this指向能够分为如下几种状况闭包
这个怎么理解呢?接下来我会一一作解析。app
通俗理解一下,就是谁调用,则this便指向谁。这里又大体分为几种状况,分别为异步
即某方法为某对象上的一个属性的属性,正常状况当改方法被调用的时候,this的指向则是挂载该方法的对象。废话很少说,直接看代码可能会更好的理解。函数
var obj = { a: 'this is obj', test: function () { console.log(this.a); } } obj.test(); // this is obj
即该函数为本身独立的函数,而不是挂载到对象上的属性(window除外),也不会被当成构造函数来使用,而仅仅是当成函数来使用,此时的this指向则是window对象。例子以下oop
var a = 'this is window' function test () { console.log(this.a); } test(); // this is window
这个咱们来理解一下,其实也很简单,咱们都知道,window对象是全局对象。其实整个代码块等同于ui
window.a = 'this is window' window.test = function test () { console.log(this.a); // 此时是window为调用者,即this会指向window } window.test();
即该函数被当成构造函数来调用,此时的this指向该构造器函数的实例对象。咱们来看一个例子,先上一个属于第二种状况的例子this
function test () { this.a = 'this is test'; console.log(this.a); console.log(this); } test(); // this is test // Window {}
按照上面的来理解,此时的this的确指向window对象,可是若是我换种形式,将其换成构造函数来调用呢,结果又会如何呢,直接上代码
function Test () { this.a = 'this is test'; console.log(this.a); console.log(this); } var test = new Test(); // this is test // Test {a: 'this is test'}
OK,好像的确没有问题了,此时的this的确指向了该构造函数的实例对象。具体这里的一些解释后面我会在原型链继承里面详细讲解。
call方法形式,fun.call(thisArg[, 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'}
和call相似,惟一的一个明显区别就是call参数为多个,apply参数则为两个,第二个参数为数组或类数组形式, fun.apply(thisArg, [argsArray])
详细介绍请猛戳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实现。
首先须要介绍的一点就是,在箭头函数自己,它是没有绑定自己的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'}) // Window {...}
这里可能会牵扯到setTimeout的一些点了,具体这里我就不讲了,想深刻了解的猛戳这里
箭头函数里面还有一些特殊的点,这里因为只说起this这一个点,其余好比不绑定arguments,super(ES6),抑或 new.target(ES6),他们都和this同样,他会找寻到当前函数的arguments等。
关于箭头函数里面的this这里也有详细的介绍,想深刻了解的能够自行阅读
其实咱们一看到原型/原型链都能和继承联想到一块儿,咱们这里就把两块先拆开来说解,这里咱们就先单独把原型/原型链拎出来。首先咱们本身问一下本身,什么是原型?什么是原型链?
好像说的有点绕,其实一张图能够解释一切
那么这个东西有怎么和指向这个概念去联系上呢?其实这里须要说起到的一个点,也是上面截图中存在的一个点,就是__proto__,我喜欢把其称为原型指针。终归到头,prototype只不过是一个属性而已,它没有什么实际的意义,最后能作原型链继承的仍是经过__proto__这个原型指针来完成的。咱们看到的所谓的继承只不过是将须要继承的属性挂载到继承者的prototype属性上面去的,实际在找寻继承的属性的时候,会经过__proto__原型指针一层一层往上找,即会去找__proto__原型指针它的一个指向。看个demo
function 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(); var test = new Test2(); test.b(); console.log(test.prototype); console.log(test);
其执行结果以下
更多关于继承的点,这里就不说起了,我会在继承这一章节作详细的讲解。那么“单独”关于原型/原型链的点就这些了。
总结:原型即prototype,它只是全部function上的一个属性而已,真正的“大佬”是__proto__,“大佬”指向谁,谁才能有言语权(固然可能由于“大佬”过于霸道,因此在ECMA-262以后才被Standard化)。
关于继承,以前我有写过一篇博文对继承的一些主流方式进行过总结。想详细了解的请点击传送门。这里咱们经过指向这个概念来从新理解一下继承。这里咱就谈两个万变不离其宗的继承方式,一个是构造函数继承,一个是原型链继承。
其实就是上面说起到的经过call/apply调用,将this指向变成thisArg,具体看上面的解释,这里直接上代码
function Test () { this.a = 'this is test'; console.log(this.a); console.log(this); } function Test2 () { Test.apply(this) // or Test.apply(this) } var test = new Test2(); // this is test // Test2 {a: 'this is test'}
通常状况,咱们作原型链继承,会经过子类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.call(this); } } var test = new Child() test.b() // monkey patch // this is Child
这个是咱们对于自定义的类进行继承并重写,那么若是是相似Array,Number,String等内置类进行继承重写的话,结果会是如何呢?关于这个话题我也有写过一篇博文进行过讲解,传送门
对于闭包,我曾经也作过总结和分享,简单的一些东西和概念这里不说起了,想了解的能够猛戳这里。和原型链那章一张,这里会摒弃掉原来的一些见解,这里我依旧经过代入指向这个概念来进行理解。
通常状况下,咱们理解闭包是这样的:“为了能够访问函数内的局部变量而定义的内部函数”。
JavaScript语言特性,每个function内都有一个属于本身的执行上下文,即特定的context指向。
内层的context上下文总能访问到外层context上下文中的变量,即每次内部的做用域能够往上层查找直到访问到当前所需访问的变量。例子以下
var a = 'this is window' function test () { var b = 'this is test' function test2 () { var c = 'this is test2'; console.log(a); console.log(b); console.log(c); } test2(); } test(); // this is window // this is test // this is test2
可是若是反过来访问的话,则不能进行访问,即变量访问的指向是当前context上下文的指向的相反方向,且不可逆。以下
function test () { var b = 'this is test'; } console.log(b); // Uncaught ReferenceError: b is not defined
这里用一个很是常见的状况做为例子,即for循环配合setTimeout的异步任务,以下
function test () { for (var i = 0; i < 4; i++) { setTimeout(function () { console.log(i); }, 0) } } test();
看到上面的例子,咱们都知道说:“答案会打印4次4”。那么为何会这样呢?我想依次打印0,1,2,3又该怎么作呢?
相信不少小伙伴们都会说,用闭包呀,就能实现了呀。对没错,的确用闭包就能实现。那么为何出现这种状况呢?
这里我简单提一下,首先这边牵扯到两个点,一个就是for循环的同步任务,一个就是setTimeout的异步任务,在JavaScript线程中,由于自己JavaScript是单线程,这个特色决定了其正常的脚本执行顺序是按照文档流的形式来进行的,即从上往下,从左往右的这样方向。每次脚本正常执行时,但凡遇到异步任务的时候,都会将其set到一个task queue(任务队列)中去。而后在执行完同步任务以后,再来执行队列任务中的异步任务。
固然对于不一样的异步任务,执行顺序也会不同,具体就看其到底属于哪一个维度的异步任务了。这里我就不详细扯Event Loop了,想更详细的了解请戳这里
回到上面咱们想要实现的效果这个问题上来,咱们通常处理方法是利用闭包进行参数传值,代码以下
function test () { for (var i = 0; i < 4; i++) { (function (e) { setTimeout(function () { console.log(e); }, 0) })(i) } } test(); // 0 -> 1 -> 2 -> 3
循环当中,匿名函数会当即执行,而且会将循环当前的 i 做为参数传入,将其做为当前匿名函数中的形参e的指向,即会保存对 i 的引用,它是不会被循环改变的。
固然还有一种常见的方式能够实现上面的效果,即从自执行匿名函数中返回一个函数。代码以下
function test () { for(var i = 0; i < 4; i++) { setTimeout((function(e) { return function() { console.log(e); } })(i), 0) } } test();
更多高阶闭包的写法这里就不一一介绍了,想了解的小伙伴请自行搜索。
文章到此差很少就要结束了
但是我也没办法,的确要结束了。下面给整篇博文作个总结吧
首先基本上JavaScript中所涉及的所谓的难点,在本文中都经过指向这个概念进行了通篇的解读,固然这是我我的对于JavaScript的一些理解,思路仅供参考。若是有什么不对的地方,欢迎各位小伙伴指出。
其实写该博文的好屡次,我想把全部的知识点所有串起来进行讲解,但又怕效果很差,因此作了一一的拆解,也进行了混合的运用。具体能领悟到多少,就要看小伙伴大家本身的了。