作用域及作用域链
规定变量和函数的可使用范围称作作用域
当访问一个变量时,编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没找到继续向上查找,直到全局作用域为止,,而作用域链,就是有当前作用域与上层作用域的一系列变量对象组成,它保证了当前执行的作用域对符合访问权限的变量和函数的有序访问。
- 在特定的场景下,特定范围内,查找变量的一套规则
- 一般情况下特指:词法作用域、静态作用域
- 一般是代码层面上的
- 作用域链:内部可以访问外部的变量,反之不行
- 变量提升机制
分类
- 全局作用域
- 函数作用域 :在函数内声明的所有变量,在函数体内是始终可见的,可以再整个函数范围内复用
- 块作用域 :是一个用来对之前的最小授权原则进行扩展的工具,将代码在函数中隐藏信息扩展为在块中
- 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(); getName(); Foo().getName(); getName(); new Foo().getName();
|
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);
|
- 对于作用域链直接通过创建态来定位作用域链
- 手动取消全局,使用块级作用域
块级作用域和暂时性死区
哪些会构成块级作用域
暂时性死区
- 从
let
声明的变量的块的第一行,到声明变量之间的这个区域,被称为暂时性死区;
- 暂时性死区存在时,会让
let
绑定这个区域,在这个区域内,无法执行该变量的其他声明
变量提升: 代码的预编译阶段
- 会对变量的内存空间进行分配
- 对变量声明进行提升,但是值为 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();
|
垃圾回收机制的策略
- 标记清除法:垃圾回收机制获取根并标记他们,然后访问并标记所有来自它们的引用,然后在访问这些对象并标记它们的引用…如此递进结束后若发现有没有标记的 (不可达的) 进行删除,进入执行环境的不能进行删除 (新生代/旧生代/generation/星历图)
- 引用计数法:当声明一个变量并给该变量赋值一个引用类型的值时候,该值的计数+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 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();
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()); console.log("o2fn", o2.fn()); console.log("o3fn", o3.fn());
|
- 在执行函数时,函数被上一级调用,上下文指向上一级
- or 直接变成公共函数,指向 window
进一步:将 console.log(‘o2fn’, o2.fn())的结果是 o2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
const o1 = { text: "o1", fn: function () { return this.text; }, };
const o2 = { text: "o2", fn: o1.fn, };
console.log("o2fn", o2.fn());
|
显式绑定 (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();
|
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); }, 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; }
Function.prototype.newBind = Function.prototype.bind || function () { const _this = this; const args = Array.prototype.slice.call(arguments); const newThis = args.shift();
return function () { return _this.apply(newThis, args); }; };
Function.prototype.newBind = Function.prototype.bind || function (context) { const fn = this; 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]; return fn.apply(this instanceof F ? this : context || this, allArgs); }; bound.prototype = new F(); return bound; };
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); } };
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));
|
闭包
定义:闭包是一个函数加上到创建函数的作用域的连接,关闭了函数的自由变量,不会被垃圾回收。
- 一个函数和他周围状态的引用捆绑在一起的组合 (函数嵌套函数时,内层函数引用了外层函数作用域下的变量,并且内层函数,在全局环境下可访问,就形成了闭包)
优点:
- 内部函数可以访问外部函数的局部变量
- 记录一些索引相关的问题
缺点:
- 变量保留在内存中,造成内存损耗 (内存泄漏只在低版本的 ie 浏览器中存在)
闭包是否会导致内存泄漏:
- 持有了不在需要的函数引用,会导致函数关联的词法环境无法销毁,导致内存泄漏
- 当多个函数共享词法环境时,会导致词法环境膨胀,从而导致出现无法触达也无法回收的内存空间
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; var obj = { number: 3, fn1: (function () { var number; this.number *= 2; number = number * 2; number = 3; return function () { var num = this.number; this.number *= 2; console.log(num); number *= 3; console.log(number); }; })(), }; var fn1 = obj.fn1; fn1.call(null); obj.fn1(); console.log(window.number);
|
1 2 3 4 5 6 7 8 9 10 11 12
| <ul><li></li><li></li><li></li><ul>
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); })(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); } console.log(count); })();
|
拆分执行
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();
|
实现私有变量
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); }, }; }
|