贪安稳就没有自由,要自由就要历些危险。只有这两条路。 --鲁迅
this是什么
this是在运行时绑定的,并不是在编写时绑定(大部分情况)
,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当函数被调用时,会创建一个活动记录(有时也成为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用凡是。传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。
function foo(){
var a = 10;
console.log(this.a);
}
var a = 20;
foo() // 20
解析: this 和 作用域注意区分,作用域是词法的与函数声明位置有关,但是this不是,this和函数的调用位置有关。
非严格模式下foo中的this指向window,所以输出的a为20。至于为什么foo的this 是指向window,我们稍后解析。
this绑定规则
默认绑定
独立函数调用。可以把这条规则看做是无法应用其他规则时的默认规则
上述 foo() // 20 例子就是本条规则的代表。在上述代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定
,无法使用其他规则。
如果使用严格模式(strict mode),则不能将全局对象用于默认绑定,因此this会被绑定为undefined。
function foo(){
'use strict';
var a = 10;
console.log(this.a);
}
var a = 20;
foo() // TypeError this is undefined
注意📢:只有foo运行在非严格模式下时,默认绑定才能绑定到全局对象;在严格模式下调用foo()则不影响默认绑定:
function foo() {
console.log(this.a);
}
(function(){
'use strict';
foo(); // 20
var a = 20
})()
隐式绑定
当函数引用有上下文对象时,隐式绑定
规则会把函数调用中的this绑定到这个上下文对象。
var obj = {
a:'test obj this',
testThis:function(){
console.log(this.a);
}
}
var a = "test this";
obj.testThis(); // => test obj this
解析: 将this绑定到了obj上,因此this.a 和 obj.a 是一样的。
隐式丢失
一个最常见的this绑定问题就是被隐式绑定
的函数会丢失绑定对象,也就是说它会应用默认绑定
,将this绑定到全局对象
或则undefined
上。
上述隐式绑定
示例代码改变一下
var obj = {
a:'test obj this',
testThis:function(){
console.log(this.a);
}
}
var a = "test this";
var testThis = obj.testThis;
testThis();
思考一下会输出什么?
结果是输出test this
,和聪明的你的答案是一样的么?
解析: testThis()其实是一个不带任何修饰的函数调用,因此应用了默认绑定
,this绑定到了全局对象
上。
🤔再思考一下,下面代码会输出什么?
var obj = {
a:'test obj this',
testThis:function(fn){
fn();
}
}
var a = "test this";
function sayA(){
console.log(this.a);
}
obj.testThis(sayA);
⏱ 思考两秒钟再看答案
结果依然是 test this
解析: 参数传递其实是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子一样。
🤔再思考一下,下面代码会输出什么?
var obj = {
a:'test obj this',
testThis:function(){
console.log(this.a);
}
}
var a = "test this";
setTimeout(obj.testThis, 0);
聪明的你一定已经知道答案了吧,试着自己分析一下吧~
显示绑定
可以直接指定this的绑定对象,我们称之为显示绑定
。
显示绑定的几种方式
- call
- apply
- bind(返回绑定this的函数)
硬绑定
先看下面一段代码
⏱ 思考一下会输出什么?
function sayName(){
console.log(this.name);
}
var obj = {
name: 'obj'
}
var name = 'global';
function foo(fn){
fn();
}
foo.call(obj,sayName);
答案是 global
解析: foo函数强制绑定了obj对象,但是fn函数在赋值时还是会发生this指向丢失导致this指向全局变量。
我们对上面代码进行一下变形
⏱ 再思考一下会输出什么?
function sayName(){
console.log(this.name);
}
var obj = {
name: 'obj'
}
var name = 'global';
function foo(fn){
fn.call(this);
}
foo.call(obj,sayName);
答案是obj
解析: 没错, 我们将fn的this硬绑定到foo的this,这种绑定是一种显示的强制绑定,也可以硬绑定
。
手写 call、apply、bind 实现
// 手写 call
Function.prototype.myCall = function(context , ...args){
const fn = this;
if(context){
context.fn = fn;
const result = context.fn(...args);
delete context.fn;
return result;
}else{
return fn(...args)
}
};
// 手写bind
Function.prototype.myBind = function(){
const params = [...arguments];
const context = params.slice(0,1)[0];
const args = params.slice(1);
const fn = this;
return function Fn(){
const newArgs = args.concat([...arguments]);
// 如果new了 bind 返回的对象, this指向指向调用函数
return this instanceof Fn ? new fn(...arguments) : fn.apply( context, newArgs);
}
}
⏱ 思考一下会输出什么呢?
var obj = {
name : 'obj'
}
function sayName () {
console.log(this.name);
}
var sayName_bind = sayName.bind(obj);
var s = new sayName_bind();
答案是undefined
解析: new 也会改变this指向,this指向调用函数,具体如何改变我们关于new 绑定
会介绍,调用函数的this中没有name,所以输出undefined
。
new 绑定
使用new来调用构造函数时,会构造一个新对象并把它绑定到构造函数调用中的this上。new是一种可以影响函数调用时this绑定行为的方法,所以称之为new 绑定
。
function foo(a){
this.a = a ;
}
var f = new foo(2);
console.log(f.a); // 2
new 调用函数都做了什么?
- 创建一个全新的对象
- 新对象被执行
Prototype
连接 - 新对象会绑定到函数调用的this
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象;如果返回了其他对象则返回其他对象。
手写new
function _new(fn, ...args) {
if (Object.prototype.toString.call(fn) === '[object Function]') {
const obj = {};
obj.__proto__ = fn.prototype;
const result = fn.apply(obj, args);
if (result) {
return result;
}
return obj;
}
throw ('fn is need to be function')
}
⏱ 思考一下会输出什么?
Function.prototype.a = 'a';
Object.prototype.b = 'b';
function Foo(){
}
var f = new Foo();
console.log(f.a);
console.log(f.b);
答案是 undefined
、b
解析: 上述介绍了new 的过程都做了什么,其中有创建新对象并将新对象绑定到函数调用的this
,所以我们能获取到对象原型链上的b
;至于a为什么没有拿到 是因为Foo的prototype指向的是Object.prototype;Foo.proto_ 指向 Foo.prototype,具体它们之间的关系会在下一篇你知道这样的原型链吗?
中讲解。
⏱ 再思考一下会输出什么?
function Foo(name){
this.name = name;
return {
name: '1'
}
}
var f = new Foo('foo');
console.log(f.name);
答案是 1
解析:还是new都做了什么中提到的 如果调用函数没有返回值则返回这个创建的新对象,如果有返回值得话,会返回函数的返回值,所以会输出 1
。
优先级
this绑定的优先级 new绑定
> 显示绑定
>隐式绑定
> 默认绑定
词法this(箭头函数)
箭头函数并不适用
上述规则。
箭头函数的this是词法的,不会动态改变,即便是使用call,bind或者apply 去绑定this值, 也不会改变箭头函数最初的this指向。
箭头函数this指向父级作用域。
⏱思考一下会输出什么?
var obj = {
a : 'obj a ',
sayThis: function(){
console.log(this.a)
},
sayThisArrow: ()=>{
console.log(this.a);
}
}
var a = 'global a ';
obj.sayThis();
obj.sayThisArrow();
答案: obj a
、global a
解析: 箭头函数this是词法的,指向父级作用域。那么当前非严格模式下父级作用域即为全局作用域。
将上述代码做下变型,⏱再来思考一下输出什么?
var obj = {
a : 'obj a ',
sayThis: function(){
return function () {
console.log(this.a)
}
},
sayThisArrow: function () {
return () =>{
console.log(this.a);
}
}
}
var a = 'global a ';
var s1 = obj.sayThis();
s1();
var s2 = obj.sayThisArrow();
s2();
聪明的你尝试自己解析一下吧 ~
⏱思考一下输出什么?
var foo = () =>{
console.log(this.a);
}
var a = 'a';
var obj = {
a: 'a1'
}
foo.call(obj);
答案: a
解析: 箭头函数的this是词法的,只跟函数定义的位置有关,显示绑定
、隐式绑定
、默认绑定
都不适用。
接下来我们测试下 new绑定
适用吗?
⏱思考一下输出什么?
var Foo = (name) => {
this.name = name;
}
var f = new Foo('foo');
console.log(f);
答案: TypeError
Foo is not a constructor, 没错箭头函数不能当做调用函数(构造函数)使用,所以箭头函数不会出现new绑定。
词法 arguments
箭头函数没有自己的arguments而是继承自父级。
⏱ 思考一下输出什么?
var foo = () => {
console.log(arguments);
}
foo(1,2);
答案: ReferenceError
arguments is not defined;
解析: 箭头函数没有自己的arguments,会报错的原因是因为非赋值变量会进行RHS查询,如果没有再作用域中找到则会报错。详细请看上一篇你不知道的作用域
。
⏱思考一下输出什么?
function foo() {
return () =>{
console.log(arguments);
}
}
foo(1,2,3)();
答案: 输出类数组 Arguments[1,2,3]