作用域及作用域链

规定变量和函数的可使用范围称作作用域

当访问一个变量时,编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没找到继续向上查找,直到全局作用域为止,,而作用域链,就是有当前作用域与上层作用域的一系列变量对象组成,它保证了当前执行的作用域对符合访问权限的变量和函数的有序访问。

  • 在特定的场景下,特定范围内,查找变量的一套规则
    • 一般情况下特指:词法作用域、静态作用域
    • 一般是代码层面上的
    • 作用域链:内部可以访问外部的变量,反之不行
    • 变量提升机制
  • 分类

    • 全局作用域
    • 函数作用域 :在函数内声明的所有变量,在函数体内是始终可见的,可以再整个函数范围内复用
    • 块作用域 :是一个用来对之前的最小授权原则进行扩展的工具,将代码在函数中隐藏信息扩展为在块中
    • eval 作用域

优先级:声明变量 > 声明普通函数 > 参数 > 变量提升

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Foo() {
getName = function () {
console.log(1);
};
return this;
}
Foo.getName = function () {
console.log(2);
};
Foo.prototype.getName = function () {
console.log(3);
};
var getName = function () {
console.log(4);
};
function getName() {
console.log(5);
}

Foo.getName(); // 2
getName(); // 4
Foo().getName(); // 1
getName(); // 1
new Foo().getName(); // 3

问题引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let a = "global";
console.log(a);

function course() {
let b = "zhaowa";
console.log(b);

session();
function session() {
let c = "this";
console.log(c);

teacher();
function teacher() {
let d = "yy";
console.log(d);

console.log("test1", b);
}
}
}
console.log("test2", b);
course();

if (true) {
let e = 111;
console.log(e);
}
console.log("test3", e);
  • 对于作用域链直接通过创建态来定位作用域链
  • 手动取消全局,使用块级作用域

块级作用域和暂时性死区

  1. 哪些会构成块级作用域

    • if
    • for
    • {…}
  2. 暂时性死区

    • let 声明的变量的块的第一行,到声明变量之间的这个区域,被称为暂时性死区;
    • 暂时性死区存在时,会让 let 绑定这个区域,在这个区域内,无法执行该变量的其他声明
  3. 变量提升: 代码的预编译阶段

    • 会对变量的内存空间进行分配
    • 对变量声明进行提升,但是值为 undefined
    • 对所有的非表达式的声明进行提升
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var bar = function () {
console.log("bar2");
};

function bar() {
console.log("bar1");
}

// 相当于是 ---------->
function bar() {
console.log("bar1");
}
var bar;
bar = function () {
console.log("bar2");
};
bar(); // bar2

垃圾回收机制的策略

  1. 标记清除法:垃圾回收机制获取根并标记他们,然后访问并标记所有来自它们的引用,然后在访问这些对象并标记它们的引用…如此递进结束后若发现有没有标记的 (不可达的) 进行删除,进入执行环境的不能进行删除 (新生代/旧生代/generation/星历图)
  2. 引用计数法:当声明一个变量并给该变量赋值一个引用类型的值时候,该值的计数+1,当该值赋值给另一个变量的时候,该计数+1,当该值被其他值取代的时候,该计数-1,当计数变为 0 的时候,说明无法访问该值了,垃圾回收机制清除该对象
    • 缺点:当两个对象循环引用的时候,引用计数无计可施。如果循环引用多次执行的话,会造成崩溃等问题。所以后来被标记清除法取代

this 上下文 context

作用域关注的函数声明在何处,而上下文,主要关注的是,函数从何处开始调用。this 是在执行时动态读取上下文决定的,而不是创建时

优先级:new > call/apply/bind > 对象调用

函数直接调用中

this 指向的是 window => 函数表达式、匿名函数、嵌套函数

1
2
3
4
5
function foo() {
console.log("函数内部this", this);
}

foo();

隐式绑定

this 的指向是调用堆栈的上一级 => 对象、数组等引用关系逻辑

1
2
3
4
5
6
7
8
9
10
function fn() {
console.log("隐式绑定", this.a);
}
const obj = {
a: 1,
fn,
};

obj.fn = fn;
obj.fn(); // 1

问题引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const foo = {
bar: 10,
fn: function () {
console.log(this.bar);
console.log(this);
},
};
// 取出
let fn1 = foo.fn;
// 执行
fn1(); // this 为 window

// 追问1, 如何改变指向
const o1 = {
text: "o1",
fn: function () {
// 直接使用上下文 - 传统分活
return this.text;
},
};

const o2 = {
text: "o2",
fn: function () {
// 呼叫领导执行 - 部门协作
return o1.fn();
},
};

const o3 = {
text: "o3",
fn: function () {
// 直接内部构造 - 公共人
let fn = o1.fn;
return fn();
},
};

console.log("o1fn", o1.fn()); // o1
console.log("o2fn", o2.fn()); // o1
console.log("o3fn", o3.fn()); // undefined
  • 在执行函数时,函数被上一级调用,上下文指向上一级
  • or 直接变成公共函数,指向 window

进一步:将 console.log(‘o2fn’, o2.fn())的结果是 o2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. 人为干涉,改变this - bind/call/apply
// 2. 不许改变this
const o1 = {
text: "o1",
fn: function () {
return this.text;
},
};

const o2 = {
text: "o2",
fn: o1.fn,
};

console.log("o2fn", o2.fn());
// this指向最后调用他的对象,在fn执行时,o1.fn抢过来挂载在自己o2fn上即可

显式绑定 (bind | apply | call)

1
2
3
4
5
6
7
8
9
10
11
function foo() {
console.log("函数内部this", this);
}
foo();

// 使用
foo.call({ a: 1 });
foo.apply({ a: 1 });

const bindFoo = foo.bind({ a: 1 });
bindFoo();

new

this 指向的是 new 之后得到的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
class Course {
constructor(name) {
this.name = name;
console.log("构造函数中的this:", this);
}

test() {
console.log("类方法中的this:", this);
}
}

const course = new Course("this");
course.test();

类中异步方法,this 有区别吗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Course {
constructor(name) {
this.name = name;
console.log("构造函数中的this:", this);
}

test() {
console.log("类方法中的this:", this);
}
asyncTest() {
console.log("异步方法外:", this);
setTimeout(function () {
console.log("异步方法内:", this); // 指向window
}, 100);
}
}

const course = new Course("this");
course.test();
course.asyncTest();
  • 执行 setTimeout 时,匿名方法执行时,效果和全局执行函数效果相同
  • 如何解决:箭头函数

bind 的原理 / 手写 bind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
function sum(a, b, c) {
console.log("sum", this);
return a + b + c;
}

// 在不考虑 new 的优先级的情况下:
// 1. 需求:手写bind => bind位置 (挂在那里) => Function.prototype
Function.prototype.newBind =
Function.prototype.bind ||
function () {
// 2. bind是什么?
const _this = this;
const args = Array.prototype.slice.call(arguments);
// args特点,第一项是新的this,第二项~最后一项函数传参
const newThis = args.shift();

// a. 返回一个函数
return function () {
// b. 返回原函数执行结果 c. 传参不变
return _this.apply(newThis, args);
};
};

// 如果考虑到 new 的优先级
// bind 返回的函数如果作为构造函数,搭配 new 关键字出现的话,这种绑定,就需要被忽略,this要绑定在实例上,也就是说,new 操作符要高于bind 绑定:

Function.prototype.newBind =
Function.prototype.bind ||
function (context) {
const fn = this;
// get bind 's params
const args = Array.prototype.slice.call(arguments, 1);
var F = function () {};
F.prototype = this.prototype;
var bound = function () {
var innerArgs = Array.prototype.slice.call(arguments);
const allArgs = [...args, ...innerArgs];
// 如果存在new, 我绑定的对象不一样了。
return fn.apply(this instanceof F ? this : context || this, allArgs);
};
bound.prototype = new F();
return bound;
};

// call
Function.prototype.newCall = function (ctx, ...args) {
ctx = ctx === null || ctx === undefined ? globalThis : Object.create(ctx);
// 用显式调用的方式,进行模拟。
const key = Symbol("temp");
Object.defineProperty(ctx, key, {
enumerable: false,
value: this,
});
if (context) {
const result = ctx[key](...args);
delete ctx[key];
return result;
} else {
return this(...args);
}
};

// apply
Function.prototype.newApply = function (context) {
// 边缘检测
// 函数检测
if (typeof this !== "function") {
throw new TypeError("Error");
}
// 参数检测
context = context || window;

// 挂载执行函数
context.fn = this;

// 执行执行函数
let result = arguments[1] ? context.fn(...arguments[1]) : context.fn();

// 销毁临时挂载
delete context.fn;
return result;
};

call、apply、bind 的区别

  • call、apply 是立即执行的: fun.call(obj) 即可,fun.bind(obj)() 与之不同,bind 返回的是函数
  • apply 第二个参数为数组,call 和 bind 需要挨个写
1
2
const arr = [1, 2, 3, 4, 5];
console.log(Math.max.apply(null, arr));

闭包

定义:闭包是一个函数加上到创建函数的作用域的连接,关闭了函数的自由变量,不会被垃圾回收。

  • 一个函数和他周围状态的引用捆绑在一起的组合 (函数嵌套函数时,内层函数引用了外层函数作用域下的变量,并且内层函数,在全局环境下可访问,就形成了闭包)
  1. 优点:

    • 内部函数可以访问外部函数的局部变量
    • 记录一些索引相关的问题
  2. 缺点:

    • 变量保留在内存中,造成内存损耗 (内存泄漏只在低版本的 ie 浏览器中存在)
      • 解决:把闭包的函数设置为 null
  3. 闭包是否会导致内存泄漏:

    • 持有了不在需要的函数引用,会导致函数关联的词法环境无法销毁,导致内存泄漏
    • 当多个函数共享词法环境时,会导致词法环境膨胀,从而导致出现无法触达也无法回收的内存空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var number = 5; // 10 // 20
var obj = {
number: 3,
fn1: (function () {
// 这里是一个立即执行函数,所以 JS 在解析的时候就会执行。
var number; // 持久化变量 3 -- 9 -- 27
this.number *= 2; // 全局定义的 number,会变成 10;
number = number * 2;
number = 3; // 这里是3;
return function () {
var num = this.number;
this.number *= 2; // 全局的又变成了 20
console.log(num); // --》【10】 --〉 3
number *= 3;
console.log(number); // --》 【9】。--〉 27
};
})(),
};
var fn1 = obj.fn1;
fn1.call(null); // fn1();
obj.fn1();
console.log(window.number);

// 10, 9, 3, 27 ,20 //
1
2
3
4
5
6
7
8
9
10
11
12
// html
<ul><li></li><li></li><li></li><ul>
//js
var lis = document.getElementByTagName('li');
for (var i = 0; i < lis.length; i++) {
(function(i){
lis[i].onclick = function(){
alert(i)
}
lis[i] = null; // 解决内存损耗
})(i)
} // 依次打印索引

函数作为返回值的场景

1
2
3
4
5
6
7
8
function mail() {
let content = "信";
return function () {
console.log(content);
};
}
const envelop = mail();
envelop();
  • 函数外部获取到了函数作用域内的变量值

函数作为参数的时候

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 单一职责
let content;
// 通用存储
function envelop(fn) {
content = 1;

fn();
}

// 业务逻辑
function mail() {
console.log(content);
}

envelop(mail);

函数嵌套

1
2
3
4
5
6
7
8
9
10
11
let counter = 0;

function outerFn() {
function innerFn() {
counter++;
console.log(counter);
// ...
}
return innerFn;
}
outerFn()();

事件处理 (异步执行) 的闭包

1
2
3
4
5
6
7
8
9
let lis = document.getElementsByTagName("li");

for (var i = 0; i < lis.length; i++) {
(function (i) {
lis[i].onclick = function () {
console.log(i);
};
})(i);
}
立即执行嵌套
1
2
3
4
5
(function immediateA(a) {
return (function immediateB(b) {
console.log(a); // 0
})(1);
})(0);
当立即执行遇上块级作用域
1
2
3
4
5
6
7
8
9
10
let count = 0;

(function immediate() {
if (count === 0) {
let count = 1;

console.log(count); // 1
}
console.log(count); // 0
})();

拆分执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function createIncrement() {
let count = 0;

function increment() {
count++;
}

let message = `count is ${count}`;

function log() {
console.log(message);
}

return [increment, log];
}
const [increment, log] = createIncrement();

increment();
increment();
increment();
log(); // 0

实现私有变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function createStack() {
return {
items: [],
push(item) {
this.item.push(item);
},
};
}

const stack = {
items: [],
push: function () {},
};

// 私有变量
function createStack() {
const items = [];
return {
push(item) {
items.push(item);
},
};
}
// Vuex store