一 、 作用域
执行环境
执行上下文(execution context),也称执行环境,定义了变量和函数有权访问的数据。环境中的所有变量和函数都保存在一个变量对象中。当某个执行环境中的代码执行完毕后,该执行环境被销毁,保存在其中的所有变量和函数定义也随之销毁。
全局执行环境是指最外围的执行环境,在Web浏览器中,就是指window对象,所有的全局对象和函数都是作为window对象的属性和方法创建的。
每个函数都有自己的执行环境,当函数执行完毕时,函数内的变量会被销毁。
例如
function printColor(){
var color = "blue";
console.log(color); //blue
}
console.log(color); //报错 "ReferenceError: color is not defined
变量color在函数printColor中建立,当函数执行完毕时,该函数的执行环境被销毁,color这个变量也被销毁,因此在全局访问是访问不到的。
没有块作用域
作用域:顾名思义,就是指变量和函数的作用范围;
块作用域, 块作用域是指用大括号{}括起来的代码会形成一个作用域,但在JavaScript中是不存在块作用域的。
例如,在C语言中:
for(int i = 1; i<5; i++){
printf("%d",i);
}
printf("%d",i);//会报错i没有定义
但在JavaScript中:
for(var i = 1; i<5; i++){
console.log(i);
}
console.log(i);//不会报错,且会打印出来 5;
这是因为在C语言中,大括号{}括住的部分形成了自己的作用域,在其中声明的变量i是无法在外部访问到的。而在javascript当中,没有块作用域的概念,在大括号内声明的变量,在大括号外也是可以访问的。
思考:这里的 i 在什么域里?这里会不会发生变量提升?
作用域链
在JavaScript中有作用域链的概念,作用域链其实就是执行环境的栈,在标识符解析的过程中,会沿着作用域链一层一层向上找。
程序在执行过程中,没进入一个新的执行环境就会将该执行环境压入执行环境栈中,每执行完毕跳出该执行环境,就会将执行环境弹出栈。
例如,以下这段程序,程序摘自《Javascript高级程序设计》:
var color = "blue";
function changeColor(){
var anotherColor = "red";
function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor; // 这里可以访问 color、 anotherColor 和 tempColor
}
// 这里可以访问 color 和 anotherColor,但不能访问 tempColor
swapColors();
}
// 这里只能访问 color
changeColor();
执行环境的压栈顺序如下:
全局作用域(var color;function changeColor)---->
changeColor()(var anotherColor; function swapColor)---->
swapColor()(var tempColor);
作用域链顺序如下, 摘自《Javascript高级程序设计》:
作用域链从右下角向左上角能访问的范围越来越小. swapColor中可以访问changeColor函数中的变量,反之则不可以。
注:自己动手试一试 color、 anotherColor 和 tempColor这三个变量在哪里可以访问,哪里不可以访问吧~
面试题
let a = 'global';
function course() {
let b = 'zhaowa';
session();
function session() {
let c = 'session';
teacher();
// 2. 函数提升
function teacher() {
// 3. 变量提升
// var d = 'yy';
// d = 'yy';
let d = 'yy';
console.log(d);
// 1. 作用域链向上查找
console.log('test1', b);
}
}
}
course();
// *************************************
// 4. 提升优先级 => 函数需要变量
// 提升维度:变量优先
// 执行维度:函数先打印
// **************************************
// 前置结论:函数是天然隔离的方案
// 5. 块级作用域
if (true) {
let e = 111;
var f = 222;
console.log(e, f);
}
console.log('f', f);
console.log('e', e);
二 、 闭包
什么是闭包
首先你要知道,闭包没什么神奇的,也没什么可怕的!
闭包是指有权访问另一个函数作用域中的变量的函数, 创建闭包的常见形式是在一个函数内部创建并返回另一个函数。
在这个例子中,返回的匿名函数就是一个闭包。
根据前面说的执行环境和作用域链,我们知道一个函数创建了一个执行上下文,函数内部的变量在函数外是访问不到的,get函数的a变量在外部是访问不到的,而闭包的作用就是使我们能够在函数外边访问到函数内部的变量,返回的匿名函数能够访问到get函数的内部变量a。
而根据作用域链知道,一个外层函数的内部函数是可以向上搜索从而访问到外层函数的变量的,就如同get函数中的匿名函数是可以访问到get函数中的变量的,因此前者就成为了后者的闭包。而这种函数嵌套是我们最常见的闭包形式。
闭包的作用域链
仍以getA()这段代码为例,当函数get()执行完毕时,其活动对象不会被销毁,因为匿名函数的作用域链仍在引用这个活动对象(引用为0时才可以被销毁,后面内存管理会说)。
当get()函数执行完毕之后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中,直到匿名函数被销毁后,get()函数的活动对象才会被销毁。因此需要手动销毁来避免内存泄漏。
var getA = get();
var res = getA();
getA = null;
由于闭包会携带父函数的作用域,因此会比其他函数占用更多的内存,从而影响性能,因此要慎重使用闭包。
闭包的副作用
作用域链的这种机制导致了闭包保存的是父函数变量的最终值。这在出现循环的时候就会产生意想不到的结果。
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
我们希望这段代码返回一个函数数组,其中每个函数都能返回对应的索引值,但实际上由于闭包最终保存的是父函数最终的变量对象,此时的i值为10,因此最终返回值都为10。
解决方案如下:
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = (function(num){
return function(){
return num;
};
})(i);
}
return result;
}
在这种解决方案中,调用每个匿名函数的时候,传入变量i,由于函数参数是按值传递的,所以会将变量i的当前值复制给参数num。而在这个匿名函数内部,又创建并返回了一个访问num的闭包,这样一来,result数组中的每个函数都有自己的num变量的一个副本,因此就可以返回不同的数值了。
三、this 上下文 context
- 我家门前有条河,门前河上有座桥,河里有群鸭。
- 我家门前有条河,'这河上'有座桥,‘这河里’有群鸭。
- this是在执行时动态读取上下文决定的
考察重点 - 各使用态中的指针指向
函数直接调用 - this指向是window
function foo() {
console.log('函数内部this', this);
}
foo()
隐式绑定 - this的指向是调用堆栈的上一级
function fn() {
console.log('隐式绑定', this.a)
}
const obj = {
a: 1,
fn
}
obj.fn = fn;
obj.fn();
面试题
const foo = {
bar: 10,
fn: function() {
console.log(this.bar);
console.log(this);
}
}
// 取出
let fn1 = foo.fn;
fn1(); // ??
const o1 = {
text: 'o1',
fn: function() {
// 直接使用上下文 - 传统分活
// console.log('o1fn_this', this);
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()); // ?
- 在执行函数时,函数背上一级调用,上下文指向上一级
- 直接变成了公共函数的话,指向window
如何console.log('o2fn', o2.fn())的结果是o2
// 1. 显式人为干涉
fn.call(o2)
// 2. 不许改变this
const o1 = {
text: 'o1',
fn: function() {
return this.text;
}
}
const o2 = {
text: 'o2',
fn: o1.fn
}
// this指向的最后调用他的对象,执行fn时,o1.fn抢过来挂载在自己的o2fn上即可
显式绑定(bind | apply | call)
function foo() {
console.log('函数this', this);
}
foo();
foo.call({a: 1});
foo.apply({a: 1});
const bindFoo = foo.bind({a: 1});
bindFoo();
追问:call、apply、bind区别
- call < = > apply 传参不同 依次传入/数组传入
- bind 返回值不同
- 面试:手写apply & bind
// 1. 需求:手写bind => bind位置 => Function.prototype => 原型
Function.prototype.newBind = function() {
// 2. bind改变原理
const _this = this;
const args = Array.prototype.slice.call(arguments);
const newThis = args.shift();
// 核心封装函数不执行
return function() {
// 执行核心apply
return _this.newApply(newThis, args);
}
}
// 2. 内层实现
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;
}
new
class Course {
constructor(name) {
this.name = name;
console.log('this', this);
}
test() {
console.log('this1', this);
}
asyncTest() {
// 异步队列 => async queue
setTimeout(function() {
console.log('this_async', this);
}, 100)
}
}
const course = new Course('this');
course.test();
course.asyncTest();
思考:如何不写bind实现bind效果
es6箭头函数
var obj = {
a:1,
func1: function(){
console.log(this.a)
},
func2: () => {
console.log(this.a)
}
}
obj.func1 = obj.func1.bind(obj) // 手动指定 func1 的 this 指向
var func1 = obj.func1
func1() // 1
var func2 = obj.func2
func2() // 箭头函数 this是静态绑定,this 指向 window
作业
- 如下代码会报错吗?如果报错请说明原因,如果不报错请说明运行结果和原因
for(var i = 1; i<5; i++){
console.log(i);
}
console.log(i);
- 如下代码输出是什么?为什么?请写出js解释器实际执行的等效代码
var v='Hello World';
(function(){
console.log(v);
var v='I love you';
})()
- 如下代码输出是什么?为什么?请写出js解释器实际执行的等效代码
function main(){
console.log(foo); // ?
var foo = 10;
console.log(foo); // ?
function foo(){
console.log("我来自 foo");
}
console.log(foo); // ?
}
main();
- 如下代码输出是什么?为什么?
var a = 10;
var foo = {
a: 20,
bar: function () {
var a = 30;
return this.a;
}
};
console.log(
foo.bar(), // ?
(foo.bar)(), // ?
(foo.bar = foo.bar)(), // ?
(foo.bar, foo.bar)() // ?
);
- 如下代码输出是什么?为什么?请写出js解释器实际执行的等效代码
var a = 10;
function main(){
console.log(a); // ?
var a = 20;
console.log(a); // ?
(function(){
console.log(a); // ?
var a = 30;
console.log(a); // ?
})()
console.log(a); // ?
}
main()
- 为什么点击所有的button打印出来的都是5而非0,1,2,3,4?要怎么修改?
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://code.jquery.com/jquery-3.1.0.js"></script>
</head>
<body>
<ul>
<li><button>0</button></li>
<li><button>1</button></li>
<li><button>2</button></li>
<li><button>3</button></li>
<li><button>4</button></li>
</ul>
</body>
</html>
var buttons = $("button")
for(var i=0;i<buttons.length;i++){
buttons[i].onclick = function(){
console.log(i)
}
}