执行上下文
个人觉得要了解指针、闭包、作用域,需要先了解执行上下文,简单总结:当JavaScript代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。
- 变量对象(Variable object,VO)
- 作用域链(Scope chain)
- this
变量对象
变量对象是与执行上下文相关的数据作用域,存储了在上下文定义的变量个函数申明
- 当函数执行时,VO会转变为AO(Activity Object),来表示变量对象
- VO\AO本质是一个东西,VO不能通过js直接被访问,函数激活时,VO被激活成AO,的各种属性和对象才能够被访问
- AO通过函数的arguments属性初始化
作用域链
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
函数的作用域在函数定义的时候就决定了。函数有一个内部属性 [[scope]],当函数激活时,进入上下文,创建VO/AO后,就会将活动对象添加到作用域链的最前端:
ScopeChain = [AO].concat([[Scope]]);
作用域链延长
大部分情况都是这样的,作用域链有多长主要看它当前嵌套的层数,但是有些语句可以在作用域链的前端临时增加一个变量对象,这个变量对象在代码执行完后移除,这就是作用域延长了。能够导致作用域延长的语句有两种:try...catch的catch块和with语句。
try...catch
let x = 1;
try {
x = x + y;
} catch(e) {
console.log(e);
}
上述代码try里面我们用到了一个没有申明的变量y,所以会报错,然后走到catch,catch会往作用域链最前面添加一个变量e,这是当前的错误对象,我们可以通过这个变量来访问到错误对象,这其实就相当于作用域链延长了。这个变量e会在catch块执行完后被销毁。
with
function f(obj, x) {
with(obj) {
console.log(x); // 1
}
console.log(x); // 2
}
f({x: 1}, 2);
with语句可以操作作用域链,可以手动将某个对象添加到作用域链最前面,查找变量时,优先去这个对象查找,with块执行完后,作用域链会恢复到正常状态。
上述代码,with里面输出的x优先去obj找,相当于手动在作用域链最前面添加了obj这个对象,所以输出的x是1。with外面还是正常的作用域链,所以输出的x仍然是2。需要注意的是with语句里面的作用域链要执行时才能确定,引擎没办法优化,所以严格模式下是禁止使用with的。
this
this是在执行时动态读取上下文决定的,而不是创建时(谁调用,指向谁,箭头函数除外)。
作用域链是创建时就确定的,this是在执行时确认的
示例:
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: 'local scope'
},
ScopeChain: [AO, [[Scope]]]
}
执行栈
是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。当JS引擎工作时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。
作用域
let a = 'global';
console.log(a);
function first() {
let b = 'first';
console.log(b);
second();
function second() {
let c = 'second';
console.log(c);
third();
// 变量或函数提升-作用域之内(调用上级函数或者变量是闭包)
console.log(e); // error: e is not defined(e申明在下级作用域)
function third() {
// let不支持提升
let d = 'third';
console.log(d);
console.log(e); // undefined: e变量提升(申明当未赋值)
// 变量通过var支持提升,声明提升
var e = 'inner';
console.log(e); // inner
console.log('test', b) // 作用域向上查找,向下传递
}
}
}
first();
//输出结果 global first second third undefined inner test first
为什么要变量提升:因为方便
function fn() {
// 函数作用域
let y = 1;
let z = 2;
if(true) {
// 块级作用域
var x = 2;
let y = 2;
}
console.log(x); // 2
console.log(y); // 1
}
fn();
console.log(x); // Error: x is not defined
// 函数作用域和块级作用域都能起到变量隔离作用(let const声明),但是使用var的情况下,仅函数作用域可以实现变量隔离
==> js module 模块化基础是函数(自执行函数IIFE)
this指针
函数中直接调用 - this指向的是执行全局
function fn() {
console.log('函数内部', this) // this -> window
function fn1() {
console.log('函数内部', this) // this -> window
}
}
隐式绑定
function fn() {
console.log('隐式绑定', this.a)
}
const obj = {
a: 1,
fn
}
obj.fn = fn;
obj.fn(); // obj 调用fn
// 输出结果: 1
拓展
const foo = {
bar: 10,
fn: function() {
console.log(this.bar);
console.log(this);
}
}
// 取出
let fn1 = foo.fn;
// 独立执行,所以 this-> window
fn1();
// 追问1: 如何改变属性指向
const o1 = {
text: 'o1',
fn: function(){
// 直接使用上下文 - 传统派活
console.log('o1fn_this', this);
return this.text;
}
}
const o2 = {
text: 'o2',
fn: function() {
// 呼叫领导执行 —— 部门协作 (任然是o1调用)
console.log('o2fn_this', this);
return o1.fn();
}
}
const o3 = {
text: 'o3',
fn: function() {
// 直接内部构造 —— 公共人 (取出了o1的方法,全局调用)
console.log('o3fn_this', this);
let fn = o1.fn;
return fn();
}
}
console.log('o1fn', o1.fn());
console.log('o2fn', o2.fn());
console.log('o3fn', o3.fn());
追问:现在我要将console.log('o2fn', o2.fn())的结果是o2
// 1. 人为干涉,改变this - bind / call / apply
o1.fn.call(o2);
// 2. 不需人为改变
const o1 = {
text: 'o1',
fn: function(){
// 直接使用上下文 - 传统派活
console.log('o1fn_this', this);
return this.text;
}
}
const o2 = {
text: 'o2',
fn: o1.fn
}
console.log('o2fn', o2.fn());
追问: call/apply/bind的区别
三者都可以改变this的指向 call传参是依次传入(类数组),apply传参是整个数组 bind也是数组传参,但是返回一个新的函数,需要额外执行
手写函数
原理或者手写函数解题思路
- 说明原理,写下注释
- 根据注释,补全代码
// 1. 需求:手写bind => bind位置(挂载在哪里)=> Function.prototype
Function.prototype.newBind = function() {
// 调用 bind 的不是函数,需要抛出异常
if (typeof this !== "function") {
throw new Error("Function.prototype.newbind is not a function");
}
// 2. bind是什么?
// 改变this
const _this = this;
// 接受参数args,第一项参数是新的this,第二项到最后一项是函数传参
const args = Array.prototype.slice.call(arguments);
const newThis = args.shift();
// 3. 返回值
return function() {
return _this.newApply(newThis, args);
}
}
// 以上bind如果通过new方法构造对象内部this指向无法获取,需要增加逻辑判断是否空函数对象,返回原始this
// var fNOP = function () {};
// var fBound = function () {
// return _this.newApply(this instanceof fNOP ? this : newThis, args);
// }
// 空对象的原型指向绑定函数的原型
// fNOP.prototype = this.prototype;
// 空对象的实例赋值给 fBound.prototype
// fBound.prototype = new fNOP();
// return fBound;
Function.prototype.newApply = function(context) {
if (typeof this !== "function") {
throw new Error("Function.prototype.newApply is not a function");
}
context = context || window;
// 挂载执行函数
context.fn = this;
let result = arguments[1]
? context.fn(...arguments[1])
: context.fn();
delete context.fn;
return result;
}
闭包
一个函数和他周围状态的引用捆绑在一起的组合
// 函数作为返回值的场景
function mail() {
let content = '信';
return function() {
console.log(content);
}
}
const envelop = mail()
envelop()
// 函数作为参数的时候
let content;
function envelop(fn) {
content = 1;
fn();
}
function mail() {
console.log(content);
}
envelop(mail);
// 函数嵌套
let counter = 0;
function outerFn() {
function innerFn() {
counter++;
console.log(counter);
}
return innerFn;
}
outerFn()();
// 立即执行函数 => js模块化的基石
let count = 0;
(function immediate(args) {
if (count === 0) {
let count = 1;
console.log(count);
}
})(args);
// 实现私有变量
function createStack() {
const items = [];
return {
push(item) {
items.push(item);
}
}
}