js中this/作用域
this
问题总结
默认绑定(函数直接调用)
非严格模式下:
function fn() {
console.log(this) // 打印window 对象
}
fn()
严格模式下:
function fn() {
'use strict'
console.log(this) // 打印undefined
}
fn()
TIP1 👉 非严格模式下,默认绑定指向全局(
node
中式global
)
var a = 1
function fn(){
var a = 2
console.log(this.a) // 1
}
fn()
var b = 1
function outer () {
var b = 2
function inner () {
console.log(this.b) // 1
}
inner()
}
outer()
const obj = {
a: 1,
fn: function() {
console.log(this.a)
}
}
obj.fn() // 1
const f = obj.fn
f() // undefined
隐式绑定(属性访问调用)
function fn () {
console.log(this.a)
}
const obj = {
a: 1
}
obj.fn = fn
obj.fn() // 1
TIP 👉 隐式绑定的
this
指的是调用堆栈的上一级(.
前面一个)
function fn () {
console.log(this.a)
}
const obj1 = {
a: 1,
fn
}
const obj2 = {
a: 2,
obj1
}
obj2.obj1.fn() // 1
面试官一般问的是一些边界 case
,比如隐式绑定失效(列举部分):
// 第一种 是前面提过的情况
const obj1 = {
a: 1,
fn: function() {
console.log(this.a)
}
}
const fn1 = obj1.fn // 将引用给了 fn1,等同于写了 function fn1() { console.log(this.a) }
fn1() // 所以这里其实已经变成了默认绑定规则了,该函数 `fn1` 执行的环境就是全局环境
// 第二种 setTimeout
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()()
// 第五种 函数赋值也会改变 this 指向,下边练习题会有 case,react 中事件处理函数为啥要 bind 一下的原因
// 第六种 IIFE
显式绑定(call
、 bind
、 apply
)
通过显式的一些方法去强行的绑定 this
上下文
function fn () {
console.log(this.a)
}
const obj = {
a: 100
}
fn.call(obj) // 100
TIP 👉 这种根本还是取决于第一个参数
但是第一个为null
的时候还是绑到全局的
function fn(){
console.log(this)
}
// boxing(装箱) -> (1 ----> Number(1))
// bind 只看第一个 bind(堆栈的上下文,上一个,写的顺序来看就是第一个)
fn.bind(1).bind(2)() // Number {1}
js bind 的实现(FROM MDN
):
// Yes, it does work with `new (funcA.bind(thisArg, args))`
if (!Function.prototype.bind) (function(){
var ArrayPrototypeSlice = Array.prototype.slice; // 为了 this
Function.prototype.bind = function(otherThis) {
// 调用者必须是函数,这里的 this 指向调用者:fn.bind(ctx, ...args) / fn
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable 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() {
// const newFn = fn.bind(ctx, 1); newFn(2) -> arguments: [1, 2]
baseArgs.length = baseArgsLength; // reset to default base arguments
baseArgs.push.apply(baseArgs, arguments); // 参数收集
return fToBind.apply( // apply 显示绑定 this
// 判断是不是 new 调用的情况,这里也说明了后边要讲的优先级问题
fNOP.prototype.isPrototypeOf(this) ? this : otherThis, baseArgs
);
};
// 下边是为了实现原型继承
if (this.prototype) { // 函数的原型指向其构造函数,构造函数的原型指向函数
// Function.prototype doesn't have a prototype property
fNOP.prototype = this.prototype; // 就是让中间函数的构造函数指向调用者的构造
}
fBound.prototype = new fNOP(); // 继承中间函数,其实这里也继承了调用者了
return fBound; // new fn()
};
})();
TIP 👉 如果函数
constructor
里没有返回对象的话,this
指向的是new
之后得到的实例
function foo(a) {
this.a = a
}
const f = new foo(2)
f.a // 2
function bar(a) {
this.a = a
return {
a: 100
}
}
const b = new bar(3)
b.a // 100
箭头函数
箭头函数这种情况比较特殊,编译期间确定的上下文,不会被改变,哪怕你 new
,指向的就是上一层的上下文,
TIP 👉 箭头函数本身是没有
this
的,继承的是外层的
function fn() {
return {
b: () => {
console.log(this)
}
}
}
fn().b() // window 与 fn 的this 指向一致
fn().b.bind(1)() // window 与 fn 的this 指向一致
fn.bind(2)().b.bind(3)() // Number{2} 与 fn 的this 指向一致
function fn() {
return {
b: function() {
console.log(this)
}
}
}
fn().b() // {b: ƒ} 指向 函数b自己的this
fn().b.bind(1)() // Number{1}
fn.bind(2)().b.bind(3)() // Number{3}
优先级
这上面的各种方式一定是有先后顺序的,同时作用于一个函数的时候,以哪一个为准呢?这取决于优先级
// 隐式 vs 默认 -> 结论:隐式 > 默认
function fn() {
console.log(this)
}
const obj = {
fn
}
obj.fn() //{fn: ƒ}
// 显式 vs 隐式 -> 结论:显式 > 隐式
obj.fn.bind(5)() // Number {5}
// new vs 显式 -> 结论:new > 显式
function foo (a) {
this.a = a
}
const obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a) // 2
// new
var baz = new bar(3)
console.log( obj1.a ) // 2
console.log( baz.a ) // 3
// 箭头函数没有 this,比较没有意义
// 1.
function foo() {
console.log( this.a ) // 2
}
var a = 2;
(function(){
"use strict" // 迷惑大家的
foo(); // 直接函数执行this 指向window
})();
// 2.
var name="the window"
var object={
name:"My Object",
getName: function(){
return this.name
}
}
object.getName() // "My Object"
(object.getName)() // "My Object"
(object.getName = object.getName)() // "the window"
(object.getName, object.getName)() // "the window"
// 3.
var x = 3
var obj3 = {
x: 1,
getX: function() {
var x = 5
return function() {
return this.x
}(); // ⚠️
}
}
console.log(obj3.getX()) // 3
// 4.
function a(x){
this.x = x
return this
}
var x = a(5) // 替换为 let 再试试
var y = a(6) // 替换为 let 再试试 // 再换回 var,但是去掉 y 的情况,再试试
console.log(x.x) // undefine
console.log(y.x) // 6
// 等价于
window.x = 5;
window.x = window;
window.x = 6;
window.y = window;
console.log(x.x) // void 0 其实执行的是 Number(6).x
console.log(y.x) // 6
作用域和闭包
存储空间、执行上下文
👉 数据是怎么存的?
本质是将数据映射成
0
1
,然后通过触发器存储这类信息(电信号)
👉 栈 和 堆 / 静态内存分配 和 动态内存分配
堆栈这里指的是存储数据结构,当然本身也可以是一种数据结构的概念(二叉堆、栈)
静态内存分配:
- 编译期知道所需内存空间大小。
- 编译期执行
- 申请到栈空间
- FILO(先进后出)
动态内存分配:
- 编译期不知道所需内存空间大小
- 运行期执行
- 申请到堆空间
- 没有特定的顺序
当控制器转到一段可执行代码的时候就会进入到一个执行上下文。执行上下文是一个堆栈结构(先进后出), 栈底部永远是全局上下文,栈顶是当前活动的上下文。其余都是在等待的状态,这也印证了JS
中函数执行的原子性
可执行代码与执行上下文是相对的,某些时刻二者等价
可执行代码(大致可以这么划分):
- 全局代码
- 函数
- eval
执行上下文(简称 EC
)中主要分为三部分内容:
-
VO
/AO
变量对象 - 作用域链
-
This
所以这个流程可以梳理出来:
-
遇到可执行代码
-
创建一个执行上下文 (可执行代码的生命周期:编译、运行)
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();
// 两个阶段:创建 - 执行
// --------------------------- 创建 ------------------------------
// 模拟 VO/AO 对象
AO(foo) {
b: void 0
}
// [[scope]] 不是作用域链,只是函数的一个属性(规范里的,不是实际实现)
// 在函数创建时被存储,静态(不变的),永远永远,直到函数被销毁
foo.[[scope]]: [VO(global)]
VO(global) {
a: void 0,
foo: Reference<'foo'>
}
// --------------------------- 调用 ------------------------------
// 可以这么去理解,近似的用一个 concant 模拟,就是将当前的活动对象放作用域链最前边
Scope = [AO|VO].concat([[Scope]])
// ---------------------------- 执行时 EC --------------------------------
EC(global) {
VO(global) {
a: void 0,
foo: Reference<'foo'>
},
Scope: [VO(global)],
// this
}
EC(foo) {
AO(foo) { // 声明的变量,参数
b: void 0
},
Scope: [AO(foo), VO(global)] // 查找顺序 -> RHS LHS
}
特殊情况:
-
Function
构造的函数[[scope]]
里只有全局的变量对象
// 证明
var a = 10;
function foo(){
var b = 20;
// 函数声明
function f1(){ // EC(f1) { Scope: [AO(f1), VO(foo), VO(g)] }
console.log(a, b);
}
// 函数表达式
var f2 = function(){
console.log(a, b);
}
var f3 = Function('console.log(a,b)')
f1(); // 10, 20
f2(); // 10, 20
f3(); // 10, b is not defined
}
foo();
-
with
&catch
&eval
本质上
eval
之类的恐怖之处是可以很方便的修改作用域链,执行完后又回归最初状态
// 这样好理解
Scope = [ withObj|catchObj ].concat( [ AO|VO ].concat( [[ scope ]] ) )
// 初始状态 [VO(foo), VO(global)]
// with 一下:[VO(with)❓, VO(foo), VO(global)]
// with 完事儿了,还要恢复 👈
var a = 15, b = 15;
with( { a: 10 } ){
var a = 30, b = 30;
alert(a); // 30
alert(b); // 30
}
alert(a); // ? answer: 15
alert(b); // 30