大纲
- 了解作用域和上下文,知道其中的区别;
- 知道闭包在实际场景中怎么用,常见的坑;
- 能将继承、作用域、闭包、模块这些概念融汇贯通,并且结合实际例子说明这几个概念怎样结合在一起;
作用域和上下文
作用域
- 作用域,简单来说,就是在特定的场景下,特定范围内,查找变量的一套规则。
- 一般情况下,我们特指:词法作用域、静态作用域。
- 一般是代码层面上的。
- 分类
- 全局作用域
- 函数作用域
- 在函数内声明的所有变量,在函数体内是始终可见的,可以再整个函数范围内复用;
- 块作用域
-
是一个用来对之前的最小授权原则进行扩展的工具,将代码在函数中隐藏信息扩展为在块中。
-
// example 1
function foo(a) {
console.log(a);
}
foo(2);
// example 2
// example 2
function bar() {
var b = 5;
};
function foo(a) {
console.log(a + b);
let b = 3;
};
foo(2);
- foo 和 bar 中的 b ,分属于两个独立且不同的作用域
- 为什么 Cannot access 'b' before initailization
- let 作为块级作用域,会存在暂时性死区;
- 为什么 NaN
- 变量提升
块级作用域和暂时性死区
-
哪些会构成块级作用域
- if
- for
- {...}
-
暂时性死区
-
从
let声明的变量的块的第一行,到声明变量之间的这个区域,被称为暂时性死区; -
暂时性死区存在时,会让
let绑定这个区域,在这个区域内,无法执行该变量的其他声明;
函数表达式
-
-
JS 是如何运行起来的?
- 代码的预编译阶段
- 会对变量的内存空间进行分配;
- 对变量声明进行提升,但是值为 undefined;
- 对所有的非表达式的声明进行提升;
- 代码的预编译阶段
var bar = function() {
console.log('bar2');
}
function bar() {
console.log('bar1');
}
// 编译后相当于是 ---------->
function bar(){
console.log('bar1');
}
var bar;
bar = function() {
console.log('bar2');
}
bar();
执行上下文
- 作用域关注的函数声明在何处,而上下文,主要关注的是,函数从何处开始调用。
闭包
函数嵌套函数时,内层函数引用了外层函数作用域下的变量,并且内层函数,在全局环境下可访问,就形成了闭包。 当函数的执行上下文,没有在原本的词法作用域内,就形成了闭包。
闭包常见的场景
- 函数式编程
- window - event handler
- setTimeout
function numGenerator() {
let num = 1;
return () => {
console.log(num++) // 123
}
}
var getNum = numGenerator();
getNum();
getNum();
getNum();
this
this 的使用规则 this的指向:this的指向是根据执行上下文动态决定的。
- 在简单调用时,this默认指向的是
window/global/undefined; 浏览器端,node端,严格模式。 - 对象调用时,绑定在对象上
- 使用 call / apply / bind 的时候,绑定在指定的参数上;
- 使用 new 关键字, 通过构造函数调用创建对象上;
- 以上三条优先级: new > call/apply/bind > 对象调用;
- 使用箭头函数时,根据外层的规则,决定 this 的指向。
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
你在开发过程中,什么情况下需要考虑 this 的指向问题?
- 存在函数式编程的时候,出现闭包的时候,就要考虑了。
bind / apply / call 的实现
// - 80分
// 在不考虑 new 的优先级的情况下:
Function.prototype.bind = Function.prototype.bind || function (context) {
const fn = this;
// get bind 's params
const args = Array.prototype.slice.call(arguments, 1);
return function(...innerArgs) {
const allArgs = [...args, ...innerArgs];
return fn.apply(context, allArgs);
}
}
/////// test ///////
function foo() {
this.baz = "bar";
return this.baz;
}
var func = foo.bind({baz:"baz"});
func() // "baz";
new func(); // baz: bar
///// 如果考虑到 new 的一个优先级 /////
// bind 返回的函数如果作为构造函数,搭配 new 关键字出现的话,这种绑定,就需要被忽略,this要绑定在实例上,也就是说,new 操作符要高于bind 绑定:
Function.prototype.bind = 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;
}
// length 属性没有修复
实现一个 call 函数
function called(context) {
const args = Array.prototype.slice.call(arguments, 1);
// 用显式调用的方式,进行模拟。
context.fn = this;
if(context) {
const result = context.fn(...args);
delete context.fn;
return result;
} else {
return this(...args)
}
}