The last time, I have learned
【THE LAST TIME】一直是我想写的一个系列,旨在厚积薄发,重温前端。javascript
也是给本身的查缺补漏和技术分享。html
欢迎你们多多评论指点吐槽。前端
系列文章均首发于公众号【全栈前端精选】,笔者文章集合详见 Nealyang/personalBlog。目录皆为暂定
讲道理,这篇文章有些拿捏很差尺度。准确的说,这篇文章讲解的内容基本算是基础的基础了,可是每每这种基础类的文章很难在啰嗦和详细中把持好。文中道不到的地方还望各位评论多多补充指正。java
相信使用过 JavaScript 库作过开发的同窗对 this
都不会陌生。虽然在开发中 this
是很是很是常见的,可是想真正吃透 this
,其实仍是有些不容易的。包括对于一些有经验的开发者来讲,也都要驻足琢磨琢磨~ 包括想写清楚 this 呢,其实还得聊一聊 JavaScript 的做用域和词法node
function foo(num) { console.log("foo:"+num); this.count++; } foo.count = 0; for(var i = 0;i<10;i++){ foo(i); } console.log(foo.count);
经过运行上面的代码咱们能够看到,foo
函数的确是被调用了十次,可是this.count
彷佛并无加到foo.count
上。也就是说,函数中的this.count并非foo.count。react
另外一种对this的误解是它不知怎么的指向函数的做用域,其实从某种意义上来讲他是正确的,可是从另外一种意义上来讲,这的确是一种误解。git
明确的说,this不会以任何方式指向函数的词法做用域,做用域好像是一个将全部可用标识符做为属性的对象,这从内部来讲他是对的,可是JavaScript代码不能访问这个做用域“对象”,由于它是引擎内部的实现es6
function foo() { var a = 2; this.bar(); } function bar() { console.log( this.a ); } foo(); //undefined
既然是全局环境,咱们固然须要去明确下宿主环境
这个概念。简而言之,一门语言在运行的时候须要一个环境,而这个环境的就叫作宿主环境
。对于 JavaScript 而言,宿主环境最为常见的就是 web 浏览器。github
如上所说,咱们也能够知道环境不是惟一的,也就是 JavaScript 代码不只仅能够在浏览器中跑,也能在其余提供了宿主环境
的程序里面跑。另外一个最为常见的就是 Node
了,一样做为宿主环境
,node
也有本身的 JavaScript
引擎:v8
.web
this
等价于 window
对象var
声明一个变量等价于给 this
或者 window
添加属性var
或者let(ECMAScript 6)
,你就是在给全局的this
添加或者改变属性值REPL
来执行程序,那么 this
就等于 global
this
并不指向 global
而是module.exports
为{}
REPL
执行一个脚本文件,用var声明一个变量并不会和在浏览器里面同样将这个变量添加给thisnode
环境里,用REPL运行脚本文件的时候,若是在声明变量的时候没有使用var
或者let
,这个变量会自动添加到global
对象,可是不会自动添加给this
对象。若是是直接执行代码,则会同时添加给global
和this
这一块代码比较简单,咱们不用码说话,改成用图说话吧!
不少文章中会将函数和方法区分开,可是我以为。。。不必啊,咱就看谁点了如花这位菇凉就行
当一个函数被调用的时候,会创建一个活动记录,也成为执行环境。这个记录包含函数是从何处(call-stack)被调用的,函数是 如何 被调用的,被传递了什么参数等信息。这个记录的属性之一,就是在函数执行期间将被使用的this引用。
函数中的 this 是多变的,可是规则是不变的。
你问这个函数:”老妹~ oh,不,函数!谁点的你?“
”是他!!!“
那么,this 就指向那个家伙!再学术化一些,因此!通常状况下!this不是在编译的时候决定的,而是在运行的时候绑定的上下文执行环境。this 与声明无关!
function foo() { console.log( this.a ); } var a = 2; foo(); // 2
记住上面说的,谁点的我!!!
=> foo()
= windwo.foo()
,因此其中this 执行的是 window 对象,天然而然的打印出来 2.
须要注意的是,对于严格模式来讲,默认绑定全局对象是不合法的,this被置为undefined。
function foo() { console.log( this.a ); } var obj2 = { a: 42, foo: foo }; var obj1 = { a: 2, obj2: obj2 }; obj1.obj2.foo(); // 42
虽然这位 xx 被点的多了。。。可是,咱们只问点他的那我的,也就是 ojb2
,因此 this.a
输出的是 42.
注意,我这里的点!不是你想的那个点哦,是 运行时~
恩。。。这,就是从良了
仍是如上文说到的,this,咱们不看在哪定义,而是看运行时。所谓的构造函数,就是关键字new
打头!
谁给我 new,我跟谁
其实内部完成了以下事情:
foo = "bar"; function testThis(){ this.foo = 'foo'; } console.log(this.foo); new testThis(); console.log(this.foo); console.log(new testThis().foo)//自行尝试
恩。。。这就是被包了
在不少书中,call、apply、bind 被称之为 this 的强绑定。说白了,谁出力,我跟谁。那至于这三者的区别和实现以及原理呢,我们下文说!
function dialogue () { console.log (`I am ${this.heroName}`); } const hero = { heroName: 'Batman', }; dialogue.call(hero)//I am Batman
上面的dialogue.call(hero)
等价于dialogue.apply(hero)
`dialogue.bind(hero)()`.
其实也就是我明确的指定这个 this 是什么玩意儿!
箭头函数的 this 和 JavaScript 中的函数有些不一样。箭头函数会永久地捕获 this值,阻止 apply或 call后续更改它。
let obj = { name: "Nealyang", func: (a,b) => { console.log(this.name,a,b); } }; obj.func(1,2); // 1 2 let func = obj.func; func(1,2); // 1 2 let func_ = func.bind(obj); func_(1,2);// 1 2 func(1,2);// 1 2 func.call(obj,1,2);// 1 2 func.apply(obj,[1,2]);// 1 2
箭头函数内的 this值没法明确设置。此外,使用 call 、 apply或 bind等方法给 this传值,箭头函数会忽略。箭头函数引用的是箭头函数在建立时设置的 this值。
箭头函数也不能用做构造函数。所以,咱们也不能在箭头函数内给 this设置属性。
虽然 JavaScript 是不是一个面向对象的语言至今还存在一些争议。这里咱们也不去争论。可是咱们都知道,类,是 JavaScript 应用程序中很是重要的一个部分。
类一般包含一个 constructor , this能够指向任何新建立的对象。
不过在做为方法时,若是该方法做为普通函数被调用, this也能够指向任何其余值。与方法同样,类也可能失去对接收器的跟踪。
class Hero { constructor(heroName) { this.heroName = heroName; } dialogue() { console.log(`I am ${this.heroName}`) } } const batman = new Hero("Batman"); batman.dialogue();
构造函数里的 this指向新建立的 类实例。当咱们调用 batman.dialogue()时, dialogue()做为方法被调用, batman是它的接收器。
可是若是咱们将 dialogue()方法的引用存储起来,并稍后将其做为函数调用,咱们会丢失该方法的接收器,此时 this参数指向 undefined 。
const say = batman.dialogue; say();
出现错误的缘由是JavaScript 类是隐式的运行在严格模式下的。咱们是在没有任何自动绑定的状况下调用 say()函数的。要解决这个问题,咱们须要手动使用 bind()将 dialogue()函数与 batman绑定在一块儿。
const say = batman.dialogue.bind(batman); say();
咳咳,技术文章,我们严肃点
咱们都说,this指的是函数运行时所在的环境。可是为何呢?
咱们都知道,JavaScript 的一个对象的赋值是将地址赋值给变量的。引擎在读取变量的时候其实就是要了个地址而后再从原地址读出来对象。那么若是对象里属性也是引用类型的话(好比 function
),固然也是如此!
而JavaScript 容许函数体内部,引用当前环境的其余变量,而这个变量是由运行环境提供的。因为函数又能够在不一样的运行环境执行,因此须要个机制来给函数提供运行环境!而这个机制,也就是咱们说到心在的 this。this的初衷也就是在函数内部使用,代指当前的运行环境。
var f = function () { console.log(this.x); } var x = 1; var obj = { f: f, x: 2, }; // 单独执行 f() // 1 // obj 环境执行 obj.f() // 2
obj.foo()
是经过obj
找到foo
,因此就是在obj
环境执行。一旦var foo = obj.foo
,变量foo
就直接指向函数自己,因此foo()
就变成在全局环境
执行.
var bar = new Foo();
var bar = foo.call(obj);
var bar = obj.foo();
var bar = foo();
var number = 2; var obj = { number: 4, /*匿名函数自调*/ fn1: (function() { var number; this.number *= 2; //4 number = number * 2; //NaN number = 3; return function() { var num = this.number; this.number *= 2; //6 console.log(num); number *= 3; //9 alert(number); }; })(), db2: function() { this.number *= 2; } }; var fn1 = obj.fn1; alert(number); fn1(); obj.fn1(); alert(window.number); alert(obj.number);
评论区留下你的答案吧~
上文中已经提到了 call
、apply
和 bind
,在 MDN
中定义的 apply
以下:
apply() 方法调用一个函数, 其具备一个指定的this值,以及做为一个数组(或相似数组的对象)提供的参数
语法:
fun.apply(thisArg, [argsArray])
如上概念 apply
相似.区别就是 apply 和 call 传入的第二个参数类型不一样。
call 的语法为:
fun.call(thisArg[, arg1[, arg2[, ...]]])
须要注意的是:
apply 的语法为:
Function.apply(obj[,argArray])
须要注意的是:
记忆技巧:apply,a 开头,array,因此第二参数须要传递数据。
请问!什么是类数组?
借!
对,就是借。举个栗子!我没有女友,周末。。。额,不,我没有摩托车🏍,周末的时候天气很好,想出去压弯。可是我有没有钱!怎么办呢,找朋友借用一下啊~达到了目的,还节省开支!
放到程序中咱们能够理解为,某一个对象没有想用的方法去实现某个功能,可是不想浪费内存开销,就借用另外一个有该方法的对象去借用一下。
说白了,包括 bind,他们的核心理念都是借用方法,已达到节省开销的目的。
代码比较简单,就不作讲解了
const arrayLike = { 0: 'qianlong', 1: 'ziqi', 2: 'qianduan', length: 3 } const arr = Array.prototype.slice.call(arrayLike);
var arr = [34,5,3,6,54,6,-67,5,7,6,-8,687]; Math.max.apply(Math, arr); Math.max.call(Math, 34,5,3,6,54,6,-67,5,7,6,-8,687); Math.min.apply(Math, arr); Math.min.call(Math, 34,5,3,6,54,6,-67,5,7,6,-8,687);
Object.prototype.toString
用来判断类型再合适不过,尤为是对于引用类型来讲。
function isArray(obj){ return Object.prototype.toString.call(obj) == '[object Array]'; } isArray([]) // true isArray('qianlong') // false
// 父类 function supFather(name) { this.name = name; this.colors = ['red', 'blue', 'green']; // 复杂类型 } supFather.prototype.sayName = function (age) { console.log(this.name, 'age'); }; // 子类 function sub(name, age) { // 借用父类的方法:修改它的this指向,赋值父类的构造函数里面方法、属性到子类上 supFather.call(this, name); this.age = age; } // 重写子类的prototype,修正constructor指向 function inheritPrototype(sonFn, fatherFn) { sonFn.prototype = Object.create(fatherFn.prototype); // 继承父类的属性以及方法 sonFn.prototype.constructor = sonFn; // 修正constructor指向到继承的那个函数上 } inheritPrototype(sub, supFather); sub.prototype.sayAge = function () { console.log(this.age, 'foo'); }; // 实例化子类,能够在实例上找到属性、方法 const instance1 = new sub("OBKoro1", 24); const instance2 = new sub("小明", 18); instance1.colors.push('black') console.log(instance1) // {"name":"OBKoro1","colors":["red","blue","green","black"],"age":24} console.log(instance2) // {"name":"小明","colors":["red","blue","green"],"age":18}
继承后面可能也会写一个篇【THE LAST TIME】。也是比较基础,不知道有没有这个必要
简易版继承
ar Person = function (name, age) { this.name = name; this.age = age; }; var Girl = function (name) { Person.call(this, name); }; var Boy = function (name, age) { Person.apply(this, arguments); } var g1 = new Girl ('qing'); var b1 = new Boy('qianlong', 100);
bind 和 call/apply 用处是同样的,可是 bind
会返回一个新函数!不会当即执行!而call/apply
改变函数的 this 而且当即执行。
原理其实就是返回闭包,毕竟 bind 返回的是一个函数的拷贝
for (var i = 1; i <= 5; i++) { // 缓存参数 setTimeout(function (i) { console.log('bind', i) // 依次输出:1 2 3 4 5 }.bind(null, i), i * 1000); }
上述代码也是一个经典的面试题,具体也不展开了。
说道 this 丢失问题,应该最多见的就是 react 中定义一个方法而后后面要加 bind(this)
的操做了吧!固然,箭头函数不须要,这个我们上面讨论过。
第一个手写我们一步一步来
Function.prototype.NealApply = function(context,args){}
Function.prototype.NealApply = function(context,args){ context = context || window; args = args || []; }
对,咱们没有黑魔法,既然绑定 this,仍是逃不掉咱们上文说的那些 this 方式
Function.prototype.NealApply = function(context,args){ context = context || window; args = args || []; //给context新增一个独一无二的属性以避免覆盖原有属性 const key = Symbol(); context[key] = this;//这里的 this 是函数 context[key](...args); }
其实这个时候咱们用起来已经有效果了。
Function.prototype.NealApply = function(context,args){ context = context || window; args = args || []; //给context新增一个独一无二的属性以避免覆盖原有属性 const key = Symbol(); context[key] = this;//这里的 this 是 testFun const result = context[key](...args); // 带走产生的反作用 delete context[key]; return result; } var name = 'Neal' function testFun(...args){ console.log(this.name,...args); } const testObj = { name:'Nealyang' } testFun.NealApply(testObj,['一块儿关注',':','全栈前端精选']);
执行结果就是上方的截图。
一上来不说优化是由于但愿你们把精力放到核心,而后再去修边幅! 罗马不是一日建成的,看别人的代码多牛批,其实也是一点一点完善出来的。
道理是这么个道理,其实要作的优化还有不少,这里咱们就把 context 的判断须要优化下:
// 正确判断函数上下文对象 if (context === null || context === undefined) { // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window) context = window } else { context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象 }
别的优化你们能够添加各类的用户容错。好比对第二个参数的类数组作个容错
function isArrayLike(o) { if (o && // o不是null、undefined等 typeof o === 'object' && // o是对象 isFinite(o.length) && // o.length是有限数值 o.length >= 0 && // o.length为非负值 o.length === Math.floor(o.length) && // o.length是整数 o.length < 4294967296) // o.length < 2^32 return true; else return false; }
打住!真的再也不多啰嗦了,这篇文章篇幅不该这样的
丐版实现:
//传递参数从一个数组变成逐个传参了,不用...扩展运算符的也能够用arguments代替 Function.prototype.NealCall = function (context, ...args) { //这里默认不传就是给window,也能够用es6给参数设置默认参数 context = context || window; args = args ? args : []; //给context新增一个独一无二的属性以避免覆盖原有属性 const key = Symbol(); context[key] = this; //经过隐式绑定的方式调用函数 const result = context[key](...args); //删除添加的属性 delete context[key]; //返回函数调用的返回值 return result; }
bind的实现讲道理是比 apply 和call 麻烦一些的,也是面试频考题。由于须要去考虑函数的拷贝。可是也仍是比较简单的,网上也有不少版本,这里就不具体展开了。具体的,我们能够在群里讨论~
Function.prototype.myBind = function (objThis, ...params) { const thisFn = this; // 存储源函数以及上方的params(函数参数) // 对返回的函数 secondParams 二次传参 let fToBind = function (...secondParams) { const isNew = this instanceof fToBind // this是不是fToBind的实例 也就是返回的fToBind是否经过new调用 const context = isNew ? this : Object(objThis) // new调用就绑定到this上,不然就绑定到传入的objThis上 return thisFn.call(context, ...params, ...secondParams); // 用call调用源函数绑定this的指向并传递参数,返回执行结果 }; if (thisFn.prototype) { // 复制源函数的prototype给fToBind 一些状况下函数没有prototype,好比箭头函数 fToBind.prototype = Object.create(thisFn.prototype); } return fToBind; // 返回拷贝的函数 };
Function.prototype.myBind = function (context, ...args) { const fn = this args = args ? args : [] return function newFn(...newFnArgs) { if (this instanceof newFn) { return new fn(...args, ...newFnArgs) } return fn.apply(context, [...args,...newFnArgs]) } }
别忘记了上面 this 的考核题目啊,同窗,该交卷了!
关注公众号: 【全栈前端精选】 每日获取好文推荐。还能够入群,一块儿学习交流