总有一些面试官喜欢问你一段不可能这么写的代码。
看一道经典且古老的面试题(学完本文后,末尾会有一道更复杂的面试题等着你哦!)javascript
代码以下: ```javascript var a = 5; var obj = { a : 10, foo: function(){ console.log(this.a) } } var bar = obj.foo obj.foo() bar() ```
我在读 Events 的 lib/events 源码的时候发现屡次用到call关键字,看来有必要搞懂 this 与 call 相关的全部内容。前端
其中几句代码是这样写的java
// 场景1: function EventEmitter() { EventEmitter.init.call(this); } // 场景2: return this.listener.call(this.target); // 场景3: return listenerCount.call(emitter, type);
3.箭头函数使用不当报错,在封装 Node.js 的一个 ORM 映射框架 Sequelize 时,封装表关联关系,因为使用箭头函数形成了读到的上下文发生变化,不是想要的 model 信息,而是指向了全局 。git
代码例子以下:程序员
var person = { "name": "koala" }; function changeJob(company, work) { this.company = company; this.work = work; }; changeJob.call(person, '百度', '程序员'); console.log(person.work); // '程序员'
文章会同步到GitHub,地址为:
程序员成长指北博客地址github
注意:本文不特殊说明的都是在浏览器中输出结果。面试
JS(ES5)里面有三种函数调用形式:数组
func(p1, p2) obj.child.method(p1, p2) func.call(context, p1, p2) // 这里先不讲 apply
好多初学者都只用到过前两种状况,并且认为前二者优于第三者。直到几天前想系统复习一下this关键字,找this相关的各类资料,在知乎看到了一个关于this的讨论。
说第三种形式才是正常的调用形式。浏览器
func.call(context,p1,p2)
其它两种都是语法糖,能够等价的变为call
形式。func(p1, p2)等价于 func.call(undefined, p1, p2);
闭包
obj.child.method(p1, p2) 等价于 obj.child.method.call(obj.child, p1, p2);
这么看咱们的函数调用只有一种形式:
func.call(context,p1,p2)
这时候是否是就知道this是什么了,就是上面的context。回到我开篇提到的面试题。
var a = 5; var obj = { a : 10, foo: function(){ console.log(this.a) } } var bar = obj.foo obj.foo() bar()
因此this指向了obj
因为没有传 context,因此 this 就是 undefined,若是是在浏览器中最后给你一个默认的 this——window 对象。若是是在 Node.js 环境中运行 this——globel对象。
在浏览器中运行结果为5 在 Node.js 环境中为 undefined。
为何在浏览器或者前端环境能够直接正常输出值,而在 Node.js 环境中输出的倒是undefined
。
看一下这段代码你可能就懂了。
(function(exports, require, module, __filename, __dirname) { { // 模块的代码 // 因此那整个代码应该在这里吧 var a = 10; function A(){ a = 5; console.log(a); console.log(this.a); } // const haha = new A(); A(); } });
先说一下 Node.js 环境下在运行某个 js 模块代码时候发生了什么,Node.js 在执行代码以前会使用一个代码封装器进行封装,例以下面所示:
(function(exports, require, module, __filename, __dirname) { { // 模块的代码 // 因此那整个代码应该在这里吧 } });
这段代码在 Node.js 环境下输出结果为5,undefined
是否是就能理解了。
这里面的this是默认绑定指向全局,当输出this.a的时候,全局应该指向这个闭包的最外层。因此输出结果式是undefined。
function fn (){ console.log(this) } var arr = [fn, fn2] arr[0]() // 这里面的 this 又是什么呢?
咱们能够把 arr0 想象为arr.0( ),虽而后者的语法错了,可是形式与转换代码里的 obj.child.method(p1, p2) 对应上了,因而就能够愉快的转换了:
arr[0]() 假想为 arr.0() 而后转换为 arr.0.call(arr) 那么里面的 this 就是 arr 了
默认绑定是函数针对的独立调用的时候,不带任何修饰的函数引用进行调用,非严格模式下 this 指向全局对象(浏览器下指向 Window,Node.js 环境是 Global ),严格模式下,this 绑定到 undefined ,严格模式不容许this指向全局对象。
var a = 'hello' var obj = { a: 'koala', foo: function() { console.log(this.a) } } let bar = obj.foo bar() // 浏览器中输出: "hello"
这段代码,bar()
就是默认绑定,函数调用的时候,前面没有任何修饰调用,也能够用以前的 call
函数调用形式理解,因此输出结果是hello
。
在函数中以函数做为参数传递,例如setTimeOut
和setInterval
等,这些函数中传递的函数中的this
指向,在非严格模式指向的是全局对象。
例子:
var name = 'koala'; var person = { name: '程序员成长指北', sayHi: sayHi } function sayHi(){ setTimeout(function(){ console.log('Hello,', this.name); }) } person.sayHi(); setTimeout(function(){ person.sayHi(); },200); // 输出结果 Hello,koala // 输出结果 Hello,koala
判断 this 隐式绑定的基本标准:函数调用的时候是否在上下文中调用,或者说是否某个对象调用函数。
例子:
var a = 'koala' var obj = { a: '程序员成长指北', foo: function() { console.log(this.a) } } obj.foo() // 浏览器中输出: "程序员成长指北"
foo 方法是做为对象的属性调用的,那么此时 foo 方法执行时,this 指向 obj 对象。
隐式绑定的另外一种状况
当有多层对象嵌套调用某个函数的时候,如 对象.对象.函数
,this 指向的是最后一层对象。
例子:
function sayHi(){ console.log('Hello,', this.name); } var person2 = { name: '程序员成长指北', sayHi: sayHi } var person1 = { name: 'koala', friend: person2 } person1.friend.sayHi(); // 输出结果为 Hello, 程序员成长指北
看完这个例子,是否是也就懂了隐式调用的这种状况。
显式绑定,经过函数call apply bind 能够修改函数this的指向。call 与 apply 方法都是挂载在 Function 原型下的方法,全部的函数都能使用。
不传参数
,例如fun.call()
,非严格模式,this默认仍是绑定到全局对象unc.call(thisArg, arg1, arg2, ...) // call 用法 func.apply(thisArg, [arg1, arg2, ...]) // apply 用法
看代码例子:
var person = { "name": "koala" }; function changeJob(company, work) { this.company = company; this.work = work; }; changeJob.call(person, '百度', '程序员'); console.log(person.work); // '程序员' changeJob.apply(person, ['百度', '测试']); console.log(person.work); // '测试'
这两个方法在调用的时候,若是咱们传入数字或者字符串,这两个方法会把传入的参数转成对象类型。
例子:
var number = 1, string = '程序员成长指北'; function getThisType () { var number = 3; console.log('this指向内容',this); console.log(typeof this); } getThisType.call(number); getThisType.apply(string); // 输出结果 // this指向内容 [Number: 1] // object // this指向内容 [String: '程序员成长指北'] // object
bind 方法
会建立一个新函数。当这个新函数被调用时,bind() 的第一个参数将做为它运行时的 this,以后的一序列参数将会在传递的实参前传入做为它的参数。(定义内容来自于 MDN )
func.bind(thisArg[, arg1[, arg2[, ...]]]) // bind 用法
例子:
var publicAccounts = { name: '程序员成长指北', author: 'koala', subscribe: function(subscriber) { console.log(subscriber + this.name) } } publicAccounts.subscribe('小红') // 输出结果: "小红 程序员成长指北" var subscribe1 = publicAccounts.subscribe.bind({ name: 'Node成长指北', author: '考拉' }, '小明 ') subscribe1() // 输出结果: "小明 Node成长指北"
使用new调用函数的时候,会执行怎样的流程:
执行构造函数中的代码
例子:
function study(name){ this.name = name; } var studyDay = new study('koala'); console.log(studyDay); console.log('Hello,', studyDay.name); // 输出结果 // study { name: 'koala' } // hello,koala
在new study('koala')
的时候,会改变this指向,将this指向指定到了studyDay对象
。
注意:若是建立新的对象,构造函数不传值的话,新对象中的属性不会有值,可是新的对象中会有这个属性。
function New(func) { var res = {}; if (func.prototype !== null) { res.__proto__ = func.prototype; } var ret = func.apply(res, Array.prototype.slice.call(arguments, 1)); if ((typeof ret === "object" || typeof ret === "function") && ret !== null) { return ret; } return res; } var obj = New(A, 1, 2); // equals to var obj = new A(1, 2);
上面介绍了 this 的四种绑定规则,可是一段代码有时候会同时应用多种规则,这时候 this 应该如何指向呢?其实它们也是有一个前后顺序的,具体规则以下:
new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
在讲箭头函数中的 this 以前,先讲一下箭头函数。
定义
MDN:箭头函数表达式的语法比函数表达式更短,而且不绑定本身的this,arguments,super或 new.target。这些函数表达式最适合用于非方法函数(non-method functions),而且它们不能用做构造函数。
常规函数能够直接拿到 arguments 属性,可是在箭头函数中若是使用 arguments 属性,拿到的是箭头函数外层函数的 arguments 属性。
例子:
function constant() { return () => arguments[0] } let result = constant(1); console.log(result()); // 1
若是咱们就是要访问箭头函数的参数呢?
你能够经过 ES6 中 命名参数 或者 rest 参数的形式访问参数
let nums = (...nums) => nums;
箭头函数与正常的函数不一样,箭头函数没有构造函数 constructor,由于没有构造函数,因此也不能使用 new 来调用,若是咱们直接使用 new 调用箭头函数,会报错。
例子:
let fun = ()=>{} let funNew = new fun(); // 报错内容 TypeError: fun is not a constructor
原型 prototype 是函数的一个属性,可是对于箭头函数没有它。
例子:
let fun = ()=>{} console.loh(fun.prototype); // undefined
上面说了没有原型,连原型都没有,天然也不能经过 super 来访问原型的属性,因此箭头函数也是没有 super 的,不过跟 this、arguments、new.target 同样,这些值由外围最近一层非箭头函数决定。
箭头函数中没有本身的this
箭头函数中没有本身的 this,箭头函数中的 this 不能用 call()、apply()、bind() 这些方法改变 this 的指向,箭头函数中的 this 直接指向的是调用函数的 上一层运行时
。
var a = 'kaola' var obj = { a: '程序员成长指北', foo: () => { console.log(this.a) } } obj.foo() // 输出结果: "koala"
看完输出结果,怕你们有疑问仍是分析一下,前面我说的箭头函数中this直接指向的是调用函数的上一层运行时
,这段代码obj.foo
在调用的时候若是是不使用箭头函数this应该指向的是 obj ,可是使用了箭头函数,往上一层查找,指向的就是全局了,因此输出结果是koala
。
什么是自执行函数?
自执行函数在咱们在代码只可以定义后,无需调用,会自动执行。开发过程当中有时间测试某一小段代码报错会使用。
代码例子以下:
(function(){ console.log('程序员成长指北') })()
或者
(function(){ console.log('程序员成长指北') }())
可是若是使用了箭头函数简化一下就只能使用第一种状况了。使用第二种状况简化会报错。
(() => { console.log('程序员成长指北') })()
应用场景其实就是开篇说到的为何写这篇文章,再重复一下。
学到这里是否是发现开篇那道面试题有点简单,已经不能知足你目前对于 this 关键字的知识储备。好的,咱们来一道复杂点的面试题。
代码以下:
var length = 10; function fn() { console.log(this.length); } var obj = { length: 5, method: function(fn) { fn(); arguments[0](); } }; obj.method(fn, 1);//输出是什么?
这段代码的输出结果是:10,2
认真读文章的应该都能正确的答出答案,每个细节文章中都讲了,我在这就不具体分析,若是不懂能够再读文章,或者直接加我好友咱们一块儿讨论,kaola 是一个乐于分享的人,期待与你共同进步。
声明:任何形式转载都请联系本人,若有问题也感谢您的指出和建议哦。