this 指向 |作用域与闭包
- 实战是检验真理的唯一标准
- 深入理解
this
- 作用域
- 闭包到底是什么
this
问题总结
这里将以实战为引子,带领大家一起总结出 this
指向问题的规律。
默认绑定(函数直接调用)
非严格模式下:
function fn() {
console.log(this)
}
fn()
严格模式下:
function fn() {
'use strict'
console.log(this)
}
fn()
TIP1 👉 非严格模式下,默认绑定指向全局(node
中式 global
)
当然,面试官可能会这样迷惑一下你:
var a = 1
function fn() {
var a = 2
console.log(this.a)
}
fn()
因为let不会把变量挂在全局环境下,那么此时的window.a就undefined
var b = 1
function outer () {
var b = 2
function inner () {
console.log(this.b)
}
inner()
}
outer()
const obj = {
a: 1,
fn: function() {
console.log(this.a)
}
}
obj.fn()
const f = obj.fn
f()
隐式绑定(属性访问调用)
这种也是非常常见的情况
function fn () {
console.log(this.a)
}
const obj = {
a: 1
}
obj.fn = fn
obj.fn()
TIP 👉 隐式绑定的 this
指的是调用堆栈的上一级(.
前面一个)
function fn () {
console.log(this.a)
}
const obj1 = {
a: 1,
fn
}
const obj2 = {
a: 2,
obj1
}
obj2.obj1.fn()
面试官一般问的是一些边界 case
,比如隐式绑定失效(列举部分):
const obj1 = {
a: 1,
fn: function() {
console.log(this.a)
}
}
const fn1 = obj1.fn
fn1()
setTimeout(obj1.fn, 1000)
function run(fn) {
fn()
}
run(obj1.fn)
var name = 'The Window';
var obj = {
name: 'My obj',
getName: function() {
return function() {
console.log(this.name)
};
}
}
obj.getName()()
显式绑定(call
、 bind
、 apply
)
通过显式的一些方法去强行的绑定 this
上下文
function fn () {
console.log(this.a)
}
const obj = {
a: 100
}
fn.call(obj)
TIP 👉 这种根本还是取决于第一个参数
但是第一个为 null
的时候还是绑到全局的
bind
这里单拎出来,因为面试常常问是吧
function fn() {
console.log(this)
}
fn.bind(1).bind(2)()
看看实现也很有趣(FROM MDN
):
if (!Function.prototype.bind) (function(){
var ArrayPrototypeSlice = Array.prototype.slice;
Function.prototype.bind = function(otherThis) {
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var baseArgs= ArrayPrototypeSlice.call(arguments, 1),
baseArgsLength = baseArgs.length,
fToBind = this,
fNOP = function() {},
fBound = function() {
baseArgs.length = baseArgsLength;
baseArgs.push.apply(baseArgs, arguments);
return fToBind.apply(
fNOP.prototype.isPrototypeOf(this) ? this : otherThis, baseArgs
);
};
if (this.prototype) {
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();
return fBound;
};
})();
new
new
也是灰常有趣,我们这里只关注其 this
指向的问题
这次,我们先实现一下
import _ from 'lodash';
function myNew(fn, ...args) {
if (typeof fn !== 'function') throw new Error('fn must be a function.')
myNew.target = fn
const temp = Object.create(fn.prototype)
const res = fn.apply(temp, ...args)
return _.isObject(res) ? res : temp
}
TIP 👉 如果函数 constructor
里没有返回对象的话,this
指向的是 new
之后得到的实例
function foo(a) {
this.a = a
}
const f = new foo(2)
f.a
function bar(a) {
this.a = a
return {
a: 100
}
}
const b = new bar(3)
b.a
箭头函数
箭头函数这种情况比较特殊,编译期间确定的上下文,不会被改变,哪怕你 new
,指向的就是上一层的上下文,
TIP 👉 箭头函数本身是没有 this
的,继承的是外层的
function fn() {
return {
b: () => {
console.log(this)
}
}
}
fn().b()
fn().b.bind(1)()
fn.bind(2)().b.bind(3)()
学习
这里只是给大家介绍一些常见的情况,更多更细节的内容还是需要大家主动的去总结,去实践,可以看看更多可参见 MDN
优先级
这上面的各种方式一定是有先后顺序的,同时作用于一个函数的时候,以哪一个为准呢?这取决于优先级
function fn() {
console.log(this)
}
const obj = {
fn
}
obj.fn()
obj.fn.bind(5)()
function foo (a) {
this.a = a
}
const obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a)
var baz = new bar(3)
console.log( obj1.a )
console.log( baz.a )
TIP 👉 优先级「new 绑」 > 「显绑」 > 「隐绑」 > 「默认绑定」
实战一波
网上随便找了几道题给大家练手
function foo() {
console.log( this.a )
}
var a = 2;
(function(){
"use strict"
foo();
})();
var name="the window"
var object={
name:"My Object",
getName: function(){
return this.name
}
}
object.getName()
(object.getName)()
(object.getName = object.getName)()
(object.getName, object.getName)()
var x = 3
var obj3 = {
x: 1,
getX: function() {
var x = 5
return function() {
return this.x
}();
}
}
console.log(obj3.getX())
function a(x){
this.x = x
return this
}
var x = a(5)
var y = a(6)
console.log(x.x)
console.log(y.x)
window.x = 5;
window.x = window;
window.x = 6;
window.y = window;
console.log(x.x)
console.log(y.x)
再骚气一点
尝试着去读一读,理解一下
this keyword
作用域和闭包
上边基础的部分完成了,我们开始抽象一下了。
存储空间、执行上下文
👉 数据是怎么存的?
本质是将数据映射成 0
1
,然后通过触发器存储这类信息(电信号)
👉 栈 和 堆 / 静态内存分配 和 动态内存分配
堆栈这里指的是存储数据结构,当然本身也可以是一种数据结构的概念(二叉堆、栈)
静态内存分配:
- 编译期知道所需内存空间大小。
- 编译期执行
- 申请到栈空间
- FILO(先进后出)
动态内存分配:
- 编译期不知道所需内存空间大小
- 运行期执行
- 申请到堆空间
- 没有特定的顺序
// rust -> wasm
fn main(){
let arr:[i32;4] = [10,20,30,40];
println!("array is {:?}",arr);
println!("array size is :{}",arr.len());
for index in 0..4 {
println!("index is: {} & value is : {}",index,arr[index]);
}
}
function main() {
let arr = [10,20,30,40];
}
👉 执行上下文 和 可执行代码
Execution context (abbreviated form — EC) is the abstract concept used by ECMA-262 specification for typification and differentiation of an executable code.
-------- ECMA262
当控制器转到一段可执行代码的时候就会进入到一个执行上下文。执行上下文是一个堆栈结构(先进后出), 栈底部永远是全局上下文,栈顶是当前活动的上下文。其余都是在等待的状态,这也印证了JS
中函数执行的原子性
可执行代码与执行上下文是相对的,某些时刻二者等价
可执行代码(大致可以这么划分):
🤔 递归 & 尾调用优化
执行上下文(简称 EC
)中主要分为三部分内容:
所以这个流程可以梳理出来:
-
遇到可执行代码
-
创建一个执行上下文 (可执行代码的生命周期:编译、运行)
2.1 初始化 VO
2.2 建立作用域链
2.3 确定 This
上下文
-
可执行代码执行阶段
3.1 参数、变量赋值、提升
3.2 函数引用
3.3 …
-
出栈
👉 作用域链
每一个执行上下文都与一个作用域链相关联。作用域链是一个对象组成的链表,求值标识符的时候会搜索它。当控制进入执行上下文时,就根据代码类型创建一个作用域链,并用初始化对象(VO/AO
)填充。执行一个上下文的时候,其作用域链只会被 with
声明和 catch
语句所影响
体会一下
var a = 20;
function foo(){
var b = 100;
alert( a + b );
}
foo();
AO(foo) {
b: void 0
}
foo.[[scope]]: [VO(global)]
VO(global) {
a: void 0,
foo: Reference<'foo'>
}
Scope = [AO|VO].concat([[Scope]])
EC(global) {
VO(global) {
a: void 0,
foo: Reference<'foo'>
},
Scope: [VO(global)],
}
EC(foo) {
AO(foo) {
b: void 0
},
Scope: [AO(foo), VO(global)]
}
特殊情况:
Function
构造的函数 [[scope]]
里只有全局的变量对象
var a = 10;
function foo(){
var b = 20;
function f1(){
console.log(a, b);
}
var f2 = function(){
console.log(a, b);
}
var f3 = Function('console.log(a,b)')
f1();
f2();
f3();
}
foo();
本质上 eval
之类的恐怖之处是可以很方便的修改作用域链,执行完后又回归最初状态
Scope = [ withObj|catchObj ].concat( [ AO|VO ].concat( [[ scope ]] ) )
var a = 15, b = 15;
with( { a: 10 } ){
var a = 30, b = 30;
alert(a);
alert(b);
}
alert(a);
alert(b);
👉 闭包
函数拥有对其词法作用域的访问,哪怕是在当前作用域之外执行
对于现代浏览器机制来说,闭包其实就是逃逸分析
闭包的数据存哪里?
存的其实是啥?
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)