前端进击的巨人(六):知否知否,须知this

前端进击的巨人(六):知否知否,须知this

常见this的误解

  1. 指向函数自身(源于this英文意思的误解)
  2. 指向函数的词法作用域(部分情况)

this的应用环境

1. 全局环境

无论是否在严格模式下,全局执行环境中(任何函数体外部)this都指向全局对象

var name = '以乐之名';
this.name;  // 以乐之名
2. 函数(运行内)环境

函数内部,this的值取决于函数被调用的方式(被谁调用)

var name = '无名氏';
function getName() {
 console.log(this.name);
}
getName();         // 无名氏 调用者是全局对象

var myInfo = {
  name: '以乐之名',
  getName: getName
};
myInfo.getName();  // 以乐之名 调用者是myInfo对象

this的正解

"this的指向是在运行时进行绑定的,而不是代码书写(函数声明)时确定!!!"

"看谁用",this的指向取决于调用者,这也是很多文章提到过的观点。"谁调用,this指向谁",只是这句话稍有偏颇,某些情况不见得都适用。

生活栗子:你的钱并不一定是你的钱,只有当你使用消费了才是你的钱 。
"看谁用"),借出去的钱就不是你的了。。。

回到正文,我们先通过栈,来理解什么是调用位置?

JavaScript中函数的调用是以栈的方式来存储,栈顶是正在运行的函数,函数调用时入栈,执行完成后出栈。

function foo() {
  // 此时的栈:全局 -> foo,调用位置在foo
  bar();
}

function bar() {
  // 此时的栈:全局 -> foo -> bar,调用位置在bar
  baz();
}

function baz() {
  // 此时的栈:全局 -> foo -> bar -> baz,调用位置在baz
  // ...
}

foo();

代码中虽然函数存在多层嵌套使用,但处于栈顶的只有正在执行的函数,也即调用者只有顶层的那一个(或最后一个),理清调用位置(调用者)有助于我们理解this

this的绑定规则

  1. 默认绑定(函数单独调用)
  2. 隐式绑定(作为对象的属性方法调用,带有执行上下文)
  3. 显示绑定(call/apply/bind
  4. new绑定(new创建实例)
  5. 箭头函数绑定(ES6新增,基于词法作用域)

默认绑定下(函数单独调用)区分严格模式

  • 非严格模式,this会指向全局对象(浏览器全局对象是window,NodeJS全局对象是global);
  • 严格模式,this指向undefined
// 非严格模式
function getName() {
  console.log(this.name);  // this指向全局对象
}
getName();  // "",并不会报错,如果外部有全局变量name,则会输出对应值

// 严格模式
function getName() {
  "use strict"
 console.log(this.name);   // this指向undefined
}
getName();  // TypeError: Cannot read property 'name' of undefined

TIPS: 严格模式中,对函数中this的影响,只在函数内声明了严格模式才会存在,如果是调用时声明严格模式则不会影响。

function getName() {
  console.log(this.name);
}

// 调用时声明严格模式
"use strict";
getName();  // ""

隐式绑定

隐式绑定中,函数一般作为对象的属性调用,带有调用者的执行上下文。因此this值取决于调用者的上下文环境。如果存在多层级属性引用,只有对象属性引用链中最顶层(最后一层)会影响调用位置,而this的值取决于调用位置。文章开头以栈来理解调用者的例子。

function getName() {
  return this.name;
}

var myInfo = {
  name: '以乐之名',
  getName: getName
};

var leader = {
  name: '大神组长'
  man: myInfo
};
leader.man.getName();  // '以乐之名'
// man 指向 myInfo,最顶层(最后一层)对象为 myInfo

apply/call的区别

apply/call方法两者类似,都可以显示绑定this,两者的区别是参数传递的方式不同。apply/call第一个参数都为要指定this的对象,不同的是apply第二个参数接受的是一个参数数组,而call从第二个参数开始接受的是参数列表。

apply语法:func.apply(thisArg, [argsArray])

call语法:func.call(thisArg, arg1, arg2, ...)

var numbers = [5, 6, 2, 3, 7];

// 求numbers的最大值

// apply
var max = Math.max.apply(null, numbers);

// call
var max = Math.max.call(null, ...numbers); // ...展开运算符

TIPS: 如果thisArg为原始值(数字,字符串,布尔值),this会指向该原始值的自动包装对象,如Number, String, Boolean

func.apply(1);
// func中的this -> Number对象;

bind的特别(柯里化的应用)

bind是ES5新增的方法,跟apply/call功能一样,可以显示绑定this。

bind语法:function.bind(thisArg[, arg1[, arg2[, ...]]])

bind()方法创建一个新的函数,在调用时设置this关键字为提供的值,并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。

-- 《Function.prototype.bind() | MDN》

"bind与apply/call的区别:apply/call传入this并立即执行函数,而bind传入this则返回一个函数,并不会立即执行,只有调用返回的函数才会执行原始函数"

bind方法是函数柯里化的一种应用,看过上篇《前端进击的巨人(五):学会函数柯里化(curry) 》的小伙伴,应该还记得"函数柯里化的特点:延迟执行,部分传参,返回一个可处理剩余参数的函数"

bind相较apply/call的优点,可以通过部分传参提前对this进行一次"永久绑定",也就是说this只需绑定一次,省却每次执行都要进行this绑定的操作。

function getName() {
  return this.name;
}

var myInfo = {
  name: '以乐之名',
  job: '前端工程师'
};

var getName = getName.bind(myInfo);
getName();  // '以乐之名';
getName(); //  '以乐之名';

// 一次性绑定,之后调用无需再修改this

TIPS: 函数柯里化可以用于参数预设,像一次性操作(判断/绑定)等。

有关函数柯里化的详解,请回阅:《前端进击的巨人(五):学会函数柯里化(curry) 》

构造函数中的this

通过new操作符可以实现对函数的构造调用。JavaScript中本身并没有"构造函数",一个函数如果没有使用new操作符调用,那么它就是个普通函数,new Func()实际上是对函数Func的"构造调用"。

在了解构造函数中的this前,有必要先了解下new实例化对象的过程。

new实例过程

  1. 创建(构造)一个全新的空对象
  2. 这个新对象会被执行"原型"链接(新对象的__proto__会指向函数的prototype)
  3. 构造函数的this会指向这个新对象,并对this属性进行赋值
  4. 如果函数没有返回其他对象,则返回这个新对象(注意构造函数的return,一般不会有return)
// 正常不带return的构造函数
function People(name, sex) {
  this.name = name;
  this.sex = sex;
}

var man = new People('亚当', '男');
var woman = new People('夏娃', '女');
// 实例化对象成功
// 构造函数带了return
function People(name, sex) {
  return 1;  // 返回的是Number对象
}
function People(name, sex) {
  return 'hello world';  // 返回的是String对象
}
function People(name, sex) {
  return function() {}
}
function People(name, sex) {
  return {};
}
// 以上并未正确实例化对象

构造函数自定义return,会造成new无法完成正确的实例化操作。如果返回值为基本类型,则返回其包装对象Number/String/Bollean

TIPS: 原型链中的this指向其实例化的对象

People.prototype.say = function() {
  console.log(`我的名字:${this.name}`);
};

var man = new People('亚当', '男');
man.say();  // 我的名字:亚当

this绑定规则的优先级

显示绑定 / new绑定 > 隐式绑定 > 默认绑定

TIPS: new无法跟apply/call同时使用

this判定步骤

  1. 函数被new操作符使用(new绑定)? YES --> this绑定的是new创建的新对象
  2. 函数通过call/apply/bind(显示绑定)? YES --> this绑定的是指定的对象
  3. 函数在某个上下文对象中调用(隐式绑定)? YES --> this绑定的是那个上下文对象
  4. 默认绑定,严格模式指向undefined,否则指向全局对象

ES6的箭头函数(词法作用域的this机制,规则之外)

箭头函数的this机制不同于传统的this机制,它采取的是另外一种机制,词法作用域的this判定规则。

// 例子一
var name = '无名氏';
var myInfo = {
  name: '以乐之名',
  getName: () => {
    console.log(this.name);
  }
};
var getName = myInfo.getName;
window.getName();     // 无名氏
myInfo.getName();     // 无名氏
// myInfo是在全局环境定义的,因此根据词法作用域,this指向全局对象

// 例子二
var name = '无名氏';
var myInfo = {
  name: '以乐之名',
  say: () => {
    setTimeout(() => {
      console.log(this.name);
    })
  }
};
myInfo.say();  // 无名氏
// 箭头函数通过作用域链来逐层查找this,最终找到全局变量myInfo,this指向全局对象

// 例子三
var name = '无名氏';
var myInfo = {
  name: '以乐之名',
  say: function() {
    setTimeout(() => {
      console.log(this.name);
    })
  }
};
myInfo.say(); // 以乐之名
// 箭头函数找到say: function(){},因此this的作用域来自myInfo

TIPS: setTimeout/setInterval/alert的调用者都是全局对象

"箭头函数的this始终指向函数定义时的this,而非执行(调用)时的this。箭头函数中的this必须通过作用域链一层一层向外查找,来确定this指向。"

扩展:箭头函数的书写规则

1. 箭头函数只能用函数表达式,不能用函数声明式写法(不包括匿名函数)
// 函数表达式
const getName = (name) => { return 'myName: ' + name };

// 匿名函数
setTimeout((name) => {
  console.log(name);
}, 1000)
2. 如果参数只有一个,可不加括号();如果没有参数或多个参数需加括号()
// 只有一个参数
const getName = name => {
  return `myName: ${name}`;
}

// 无参数
const getName = () => {
  return 'myName: "以乐之名"';
}

// 多参数
const getName = (firstName, lastName) => {
  return `myName: ${firstName} ${lastName}`;
}
3. 函数体只有一个可不加花括号{}
const getName = name => return `myName: ${name}`;
4. 函数体没有花括号{},可不写return,会自动返回
const getName = name => `myName: ${name}`;

参考文档:

本文首发Github,期待Star!
https://github.com/ZengLingYong/blog

作者:以乐之名
本文原创,有不当的地方欢迎指出。转载请指明出处。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • 函数和对象 1、函数 1.1 函数概述 函数对于任何一门语言来说都是核心的概念。通过函数可以封装任意多条语句,而且...
    道无虚阅读 4,521评论 0 5
  • 特别说明,为便于查阅,文章转自https://github.com/getify/You-Dont-Know-JS...
    杀破狼real阅读 686评论 0 1
  • 关于 this this 关键字是 JavaScript 中最复杂的机制之一。它是一个很特别的关键字,被自动定义在...
    游学者灬墨槿阅读 571评论 1 2
  • 1.概念 在JavaScript中,this 是指当前函数中正在执行的上下文环境,因为这门语言拥有四种不同的函数调...
    BluesCurry阅读 1,120评论 0 2
  • 任何技术的发展到最后被广泛应用时,最终的目的是为了提升原来的效率和提高原来的生产率,很多高精尖的技术是为了人类的未...
    赵程冲阅读 229评论 0 0