|
| 1 | +## 一、引子 |
| 2 | + |
| 3 | +```js |
| 4 | +var name = "Heternally"; |
| 5 | +var obj = { |
| 6 | + name: "zl", |
| 7 | + foo: function() { |
| 8 | + console.log(this.bar); |
| 9 | + } |
| 10 | +}; |
| 11 | + |
| 12 | +var foo = obj.foo; |
| 13 | + |
| 14 | +obj.foo(); // "zl" |
| 15 | +foo(); // "Heternally" |
| 16 | +``` |
| 17 | +可以看到上面代码中,`obj.foo`和`foo`都指向同一个函数,但是执行结果却不一样;产生这种差异的原因,就在于函数体内部使用了`this`关键字; |
| 18 | + |
| 19 | +相信很多文章都会说,**`this`指向的是函数运行时所在的环境。** |
| 20 | + |
| 21 | +所以上面的问题,对`obj.foo()`来说,`foo`运行在`obj`环境中,所以`this`指向`obj`;对于`foo()`来说,`foo`运行在全局环境下,所以在非严格模式下`this`指向`window`,所以导致了两者运行的结果不同; |
| 22 | + |
| 23 | +看到这有的同学可能就有疑问了,函数的运行环境是如何判定的?为什么`obj.foo()`就是在`obj`环境,为何`var foo = obj.foo;`,`foo()`就在全局环境执行了; |
| 24 | + |
| 25 | +接下来就跟同学们讲解一下为何`Javascript`这样处理,带你彻底理解`this`。 |
| 26 | + |
| 27 | +## 二、为何设计`this`关键字 |
| 28 | + |
| 29 | +要理解`this`的设计,我们需要先了解`Javascript`中内存的数据结构; |
| 30 | + |
| 31 | +`Javascript`内置七种数据类型,可以分为**基本数据类型**和**对象数据类型**,在这里我们主要讲解一下**对象数据类型**在内存中的存储方式; |
| 32 | + |
| 33 | +```js |
| 34 | +var obj = { name: 'Heternally'}; |
| 35 | +``` |
| 36 | +`Javascript`引擎在处理上面代码时,会在**堆内存**中,生成一个对象`{ name: 'Heternally'}`,然后把这个对象在内存中的地址赋值给变量`obj`。所以在读取`obj.name`时,需要先从变量`obj`拿到地址,然后再从对应地址中拿到对象,再返回它的`name`属性。 |
| 37 | + |
| 38 | +可能看到这里会有同学要问这跟`this`有啥关系,别急,接下来重点来啦: |
| 39 | + |
| 40 | +对象的属性可能是一个函数,当引擎遇到对象属性是函数的情况,会将函数单独保存在**堆**中,然后再将函数的地址赋值给对象属性;而`Javascript`是允许在函数体内引用当前环境的其他变量,那么问题来了,函数可以在不同的运行环境执行,所以我们就需要一种机制,能够在函数内获得当前运行环境,由此诞生了`this`,**它的设计目的就是指向函数运行时所在的环境。** |
| 41 | + |
| 42 | +理解了`this`的设计,剩下的难点就是如何正确的判定它所指向的环境。 |
| 43 | + |
| 44 | +## 三、如何正确判定`this`指向 |
| 45 | + |
| 46 | +总结了`this`的绑定规则总共是有下面5种: |
| 47 | +* 1、默认绑定(严格/非严格模式) |
| 48 | +* 2、隐式绑定 |
| 49 | +* 3、显式绑定 |
| 50 | +* 4、new绑定 |
| 51 | +* 5、ES6箭头函数绑定 |
| 52 | + |
| 53 | +### 1、默认绑定 |
| 54 | + |
| 55 | +#### 1.1 严格模式 |
| 56 | + |
| 57 | +在严格模式下,不能将全局对象`window`作为默认绑定,此时`this`会绑定到`undefined`,但是在严格模式下调用函数则不会影响默认绑定。 |
| 58 | + |
| 59 | +```js |
| 60 | +(() => { |
| 61 | + "use strict" |
| 62 | + function foo() { |
| 63 | + console.log(this.name); |
| 64 | + }; |
| 65 | + var name = "Heternally"; |
| 66 | + foo(); |
| 67 | +})(); |
| 68 | + |
| 69 | +// Uncaught TypeError: Cannot read property 'name' of undefined at foo |
| 70 | +``` |
| 71 | + |
| 72 | +```js |
| 73 | +var name = 'Heternally'; |
| 74 | +function foo() { |
| 75 | + console.log(this.name); |
| 76 | +}; |
| 77 | + |
| 78 | +(() => { |
| 79 | + "use strict" |
| 80 | + foo(); |
| 81 | +})(); |
| 82 | + |
| 83 | +// Heternally |
| 84 | +``` |
| 85 | + |
| 86 | +#### 1.2 非严格模式 |
| 87 | + |
| 88 | +在非严格模式下,此时`this`就指向**全局对象** |
| 89 | + |
| 90 | +```js |
| 91 | +var name = 'Heternally'; |
| 92 | +function foo() { |
| 93 | + console.log(this.name); |
| 94 | +} |
| 95 | + |
| 96 | +foo(); // Heternally |
| 97 | +``` |
| 98 | + |
| 99 | +### 2、隐式绑定 |
| 100 | + |
| 101 | +当函数作为对象的属性存在,通过**对象属性执行函数**时,此时隐式绑定规则会将`this`绑定到对象上; |
| 102 | + |
| 103 | +```js |
| 104 | +var name = 'Heternally'; |
| 105 | +function foo() { |
| 106 | + console.log(this.name); |
| 107 | +} |
| 108 | + |
| 109 | +var obj = { |
| 110 | + name: 'zl', |
| 111 | + foo, |
| 112 | +} |
| 113 | + |
| 114 | +obj.foo(); // zl |
| 115 | +``` |
| 116 | + |
| 117 | +注意上面代码中函数执行方式是通过**对象属性**执行 |
| 118 | + |
| 119 | +```js |
| 120 | +var name = 'Heternally'; |
| 121 | +function foo() { |
| 122 | + console.log(this.name); |
| 123 | +} |
| 124 | + |
| 125 | +var obj = { |
| 126 | + name: 'zl', |
| 127 | + foo, |
| 128 | +} |
| 129 | + |
| 130 | +foo(); // Heternally |
| 131 | + |
| 132 | +var foo1 = obj.foo; |
| 133 | + |
| 134 | +foo1(); // Heternally |
| 135 | + |
| 136 | +obj.foo(); // zl |
| 137 | +``` |
| 138 | +由上面代码可以发现,通过赋值操作后执行函数,会应用默认绑定,此时在非严格模式下`this`会指向全局对象。 |
| 139 | + |
| 140 | +同样的,函数传参也是一种隐式赋值,此时在回调函数中会丢失`this`绑定。 |
| 141 | + |
| 142 | +```js |
| 143 | +function foo() { |
| 144 | + console.log(this.name); |
| 145 | +} |
| 146 | + |
| 147 | +function Foo(fn) { |
| 148 | + fn(); |
| 149 | +} |
| 150 | + |
| 151 | +var obj = { |
| 152 | + name: 'zl', |
| 153 | + foo, |
| 154 | +} |
| 155 | + |
| 156 | +var name = 'Heternally'; |
| 157 | + |
| 158 | +Foo(obj.foo); // Heternally |
| 159 | +``` |
| 160 | + |
| 161 | +### 3、显式绑定 |
| 162 | + |
| 163 | +**通过 `call` `apply` `bind`绑定** |
| 164 | + |
| 165 | +相信同学们都知道这三个方法的作用,这边就主要拿`call`来举例; |
| 166 | + |
| 167 | +> 一句话介绍`call`:使用一个指定的`this`和若干个指定的参数调用某个函数或方法。 |
| 168 | +
|
| 169 | +在讲解`call`显示绑定之前,我们先想一下`call`做了哪些事儿。 |
| 170 | + |
| 171 | +* 将函数设为对象的属性 |
| 172 | +* 指定函数的this,并进行传参 |
| 173 | +* 执行&删除函数 |
| 174 | +* 判定如果没有指定要绑定的this,非严格模式下默认指向全局对象 |
| 175 | + |
| 176 | +可以看到值调用`call`方法后,会将`this`绑定到指定对象,所以称为**显示绑定** |
| 177 | + |
| 178 | +```js |
| 179 | +function foo() { |
| 180 | + console.log(this.name); |
| 181 | +} |
| 182 | + |
| 183 | +var obj = { |
| 184 | + name: 'Heternally', |
| 185 | +} |
| 186 | + |
| 187 | +var obj1 = { |
| 188 | + name: 'Heternally1' |
| 189 | +} |
| 190 | + |
| 191 | +var name = 'zl'; |
| 192 | + |
| 193 | +foo.call(obj); // Heternally 调用call方法后强行将foo函数的this指向来obj对象上 |
| 194 | + |
| 195 | +foo.call(obj).call(obj1); // Heternally 多次调用call方法,以第一次为准 |
| 196 | + |
| 197 | +foo.call(obj1).call(obj); // Heternally1 |
| 198 | + |
| 199 | +foo.call();// zl 没有传入指定对象,所以this默认指向全局对象 |
| 200 | +``` |
| 201 | +> 如果call、apple、bind的绑定对象是null或者undefined,那么实际上在调用时这些值都会被忽略,所以使用的是默认绑定规则 |
| 202 | +
|
| 203 | + |
| 204 | +### 4、通过new绑定 |
| 205 | + |
| 206 | +我们先看看构造函数在使用`new`后,执行了什么操作: |
| 207 | +* 它创建(构造)了一个全新的对象 |
| 208 | +* 它会被执行[[Prototype]](也就是__proto__)链接 |
| 209 | +* 它使this指向新创建的对象 |
| 210 | +* 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上 |
| 211 | +* 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用将返回该对象引用 |
| 212 | + |
| 213 | +所以在使用`new`调用构造函数后,会构造一个新对象并将函数调用中的`this`绑定到新对象上。 |
| 214 | + |
| 215 | +```js |
| 216 | +var name = 'zl'; |
| 217 | + |
| 218 | +function foo(name) { |
| 219 | + this.name = name; |
| 220 | +} |
| 221 | + |
| 222 | +var bar = { |
| 223 | + name: 'object', |
| 224 | + foo1: new foo('Heternally'), |
| 225 | +} |
| 226 | + |
| 227 | +console.log(bar.foo1.name); // Heternally |
| 228 | +``` |
| 229 | + |
| 230 | +构造函数是存在返回值的,可以将函数的返回值分成三种情况: |
| 231 | + |
| 232 | +* 返回一个对象 |
| 233 | +* 没有返回值,即默认返回`undefined` |
| 234 | +* 返回基本数据类型 |
| 235 | + |
| 236 | +```js |
| 237 | +1、返回一个对象 |
| 238 | +function Foo(name,age) { |
| 239 | + this.name = name; |
| 240 | + return { |
| 241 | + age |
| 242 | + } |
| 243 | +} |
| 244 | + |
| 245 | +var bar = new Foo("Heternally","18"); |
| 246 | + |
| 247 | +bar.name; // undefined |
| 248 | + |
| 249 | +bar.age; // 18 |
| 250 | + |
| 251 | +2、没有返回值 |
| 252 | + |
| 253 | +function Foo(name,age) { |
| 254 | + this.name = name; |
| 255 | +} |
| 256 | + |
| 257 | +var bar = new Foo("Heternally","18"); |
| 258 | + |
| 259 | +bar.name; // Heternally |
| 260 | + |
| 261 | +bar.age; // undefined |
| 262 | + |
| 263 | +3、返回基本数据类型 |
| 264 | + |
| 265 | +function Foo(name,age) { |
| 266 | + this.name = name; |
| 267 | + return 123 |
| 268 | +} |
| 269 | + |
| 270 | +var bar = new Foo("Heternally","18"); |
| 271 | + |
| 272 | +bar.name; // Heternally |
| 273 | + |
| 274 | +bar.age; // undefined |
| 275 | +``` |
| 276 | + |
| 277 | +所以使用`new`绑定时,需要判断函数返回的值是否为一个对象,如果是对象,那么`this`会绑定到返回的对象上。 |
| 278 | + |
| 279 | +### 5、ES6箭头函数绑定 |
| 280 | + |
| 281 | +ES6新增了一种函数类型:箭头函数,箭头函数调用时无法使用上面四种规则了,它和普通函数最不同的一点就是对于箭头函数的`this`指向,是根据它外层(函数/全局)作用域来决定。 |
| 282 | + |
| 283 | +```js |
| 284 | +function foo() { |
| 285 | + return (name) => { |
| 286 | + console.log(this.name); |
| 287 | + } |
| 288 | +} |
| 289 | + |
| 290 | +var obj = { |
| 291 | + name: 'Heternally' |
| 292 | +} |
| 293 | + |
| 294 | +var obj1 = { |
| 295 | + name: 'text' |
| 296 | +} |
| 297 | + |
| 298 | +var name = 'zl'; |
| 299 | + |
| 300 | +var foo1 = foo(); |
| 301 | +foo1(); // zl |
| 302 | + |
| 303 | +var foo2 = foo.call(obj); |
| 304 | +foo2(); // Heternally |
| 305 | + |
| 306 | +foo2.call(obj1); // Heternally 可以看到,箭头函数的`this`绑定后无法被修改 |
| 307 | + |
| 308 | +``` |
| 309 | + |
| 310 | + |
| 311 | +### 6、规则优先级 |
| 312 | + |
| 313 | +```js |
| 314 | +1、new绑定 |
| 315 | +var obj = new Foo(); |
| 316 | +this绑定新的对象上 |
| 317 | + |
| 318 | +2、显示绑定 |
| 319 | +var obj = foo.call(bar); |
| 320 | +this绑定到指定对象上,若指定对象为null/undefined或着没传,则使用默认绑定规则 |
| 321 | + |
| 322 | +3、隐式绑定 |
| 323 | +var obj = bar.foo(); |
| 324 | +this绑定到调用方法的对象上 |
| 325 | + |
| 326 | +4、默认绑定 |
| 327 | +foo(); |
| 328 | +this在严格模式下绑定到undefined |
| 329 | +在非严格模式下绑定到全局对象 |
| 330 | +``` |
| 331 | + |
| 332 | +> [原文链接](https://github.com/HEternally/Blog/blob/master/this%E5%85%A8%E9%9D%A2%E8%A7%A3%E6%9E%90.md) |
0 commit comments