主讲:麓一
专业术语
- 常量、变量、数据类型
- 形参、实参
- 匿名函数、具名函数、自执行函数
- 函数声明、函数表达式
- 堆、栈
- 同步、异步、进程、线程
执行上下文
当函数执行时,会创建⼀个称为执行上下文(execution contex)的环境,分为 创建 和 执行 2 个阶段。
创建阶段
创建阶段,指函数被调用但还未执行任何代码时,此时创建一个拥有 3 个属性的对象:
executionContext = {
scopeChain: {}, // 创建作用域链(scope chain)
variableObject: {}, // 初始化变量、函数、形参
this: {}, // 指定this
};
代码执行阶段
代码执行阶段主要的工作是:
- 分配变量、函数的引用,赋值;
- 执行代码;
执行上下文栈
- 浏览器中的
JS解释器是单线程的,相当于浏览器中同一时间只能做一件事情; - 代码中只有一个全局执行上下文,和无数个函数执行上下文,这些组成了执行上下文栈(
Execution Stack); - 一个函数的执行上下文,在函数执行完毕后,会被移出执行上下文栈;
function c() {
console.log('ok');
}
function a() {
function b() {
c();
}
b();
}
a();
这个例子的执行上下文栈是这样的:
作用域
-
作用域,简单来说,就是在特定的场景下,特定范围内,查找变量的一套原则;
-
一般情况下,我们特指:词法作用域、静态作用域;
-
一般是代码层面上的;
-
-
分类:
- 全局作用域;
- 函数作用域;
- 在函数内声明的所有变量,在函数体内是始终可见的,可以在整个函数范围内复用;
- 块作用域;
- 是一个用来对之前的最小授权原则进行扩展的工具,将代码在函数中隐藏信息扩展为在块中;
// example 1
function foo(a) {
console.log(a); // 2
}
foo(2);
// example 2
function bar() {
var b = 5;
}
function foo(a) {
console.log(a + b); // NaN
var b = 3;
}
foo(2);
function foo2(a) {
console.log(a + b); // ReferenceError: Cannot access 'b' before initialization
let b = 3;
}
foo2(2);
foo和bar中的b,分属于两个独立且不同的作用域;- 为什么
Cannot access 'b' before initialization?let作为块级作用域,会存在暂时性死区;
- 为什么
NaN?- 变量提升;
块级作用域和暂时性死区
-
哪些会构成块级作用域:
iffor{...}
-
暂时性死区:
- 从
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();
JS 中有全局作用域、函数作用域,ES6 中又增加了块级作用域。作用域的最大用途就是隔离变量和函数,并控制他们的生命周期。作用域是在函数执行上下文【创建时】定义好的,不是函数执行时定义的。
函数在定义的时候,就已经确定了函数体内部自由变量的作用域。**js 没有块级作用域,除了全局作用域外,只有函数才能创建作用域。**作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
// 定义时!!!
// 一个函数找作用域链的时候,是找定义时候的作用域链
function demo(num) {
var getData = function getData() {};
console.log('myName >>> ', myName); // 报错:ReferenceError: myName is not defined
function c() {}
}
function outer() {
var myName = 'xiaowa';
demo(100);
}
outer();
function a() {
return function b() {
var myname = 'b';
console.log(myname); // b
};
}
function c() {
var myname = 'c';
b();
}
var b = a();
c();
/*
输出结果:b
*/
// 块级作用域
// 去掉函数 b 中的 myname 声明后
function a() {
return function b() {
// var myname = 'b';
console.log(myname); // 这⾥会报错:ReferenceError: myname is not defined
};
}
function c() {
var myname = 'c';
b();
}
var b = a();
c();
let(bable 编译)
function demo(num) {
console.log('name >>>> ', name);
// const let
let name = 'xiaowa';
}
demo(100);
babel 编译后结果:会把 let/const 编译成 var
/******/ (() => {
// webpackBootstrap
var __webpack_exports__ = {};
function demo(num) {
console.log('name >>>> ', name); // const let
var name = 'xiaowa';
}
demo(100);
/******/
})();
let 在块级作用域中,babel 编译后:
function demo(num) {
{
let myName = 'xiaowa';
console.log('myName >>>> ', myName);
}
console.log('myName >>>> ', myName); // 编译后,此处的输出会报错,ReferenceError: myName is not defined
}
demo(100);
/******/ (() => {
// webpackBootstrap
var __webpack_exports__ = {};
function demo(num) {
{
var _myName = 'xiaowa';
console.log('myName >>>> ', _myName);
}
console.log('myName >>>> ', myName);
}
demo(100);
/******/
})();
作用域链
当一个块或函数嵌套在另一个块或函数中时,就会生了作用域的嵌套。在当前函数中如果 JS 引擎无法找到某个变量,就会往上一级嵌套的作用域中去寻找,直到找到该变量或抵达全局作用域,这样的链式关系就称为 作用域链(Scope Chain)。
作用域有上下级关系,上下级关系的确定就看函数在哪个作用域下创建的,当代码在一个环境中执行,会创建变量对象的一个作用域链。当访问变量时,会一级一级向上寻找变量定义,直到找到它。若一直寻找到全局作用域还找不到就会报 'xxx is not defined' 的错误。
闭包
函数嵌套函数时,内层函数引用了外层函数作用域下的变量,并且内层函数,在全局环境下可访问,就形成了闭包。 当函数的执行上下文,没有在原本的词法作用域内,就形成了闭包。
高级程序设计三中:闭包是指有权访问另外一个函数作用域中的变量的函数,可以理解为(能够读取其他函数内部变量的函数)。
function outer() {
var top = xxxx;
function inner() {
xxx.innerHTML = top;
}
}
内部函数访问到外部的变量了,就叫闭包了。
function outer() {
var top = 'yuanxin';
function inner() {
console.log('top >>>> ', top);
}
inner();
}
outer();
/*
内部函数访问到外部的变量了,就叫闭包了
*/
function outer() {
var top = 'yuanxin';
let count = 0;
return function () {
count++;
console.log('top >>>> ', top, count);
};
}
let func = outer();
func();
func();
/*
top >>>> yuanxin 1
top >>>> yuanxin 2
*/
平时用在哪?
- 封装私有变量;
- 存储变量;
封装私有变量(AMD 的框架等都使用)
普通的定义类的方式
// 普通的定义类的方式
function Person() {
this._attackVolume = 100;
}
Person.prototype = {
attack() {
console.log(this._attackVolume - 10); // 90
},
};
var person = new Person();
console.log(person._attackVolume); // 100
person.attack();
工厂方法
构造函数和工厂函数都可以创造对象,构造函数需要
new一个对象,工厂函数不用,这两种方式没有区别。
// 工厂方法
// 构造函数和工厂函数都可以创造对象,构造函数需要 new 一个对象,工厂函数不用,这两种方式没有区别
function Person() {
var _attackVolume = 100;
return {
attack() {
console.log(_attackVolume); // 100
console.log(this._attackVolume); // undefined
},
};
}
var person = Person();
console.log(person._attackVolume); // undefined
person.attack();
存储变量
// 封装的时候
function getListDataManager() {
// 外层 scope 中定义⼀个变量
let localData = null;
return {
getData() {
// ⾥⾯的函数使⽤外层的变量,⽽且是反复使⽤
if (localData) {
return Promise.resolve(localData);
}
return fetch('xxxx').then(data => (localData = data.json()));
},
};
}
// ⽤的时候
const listDataManager = getListDataManager();
button.onclick = () => {
// 每次都会去获取数据,但是有可能是获取的缓存的数据
text.innerHTML = listDataManager.getData();
};
window.onscroll = () => {
// 每次都会去获取数据,但是有可能是获取的缓存的数据
text.innerHTML = listDataManager.getData();
};
拆分执行
function createIncrement() {
let count = 0;
function increment() {
count++;
}
let message = `count is ${count}`;
function log() {
console.log(message); // count is 0
console.log(count); // 3
}
return [increment, log];
}
const [increment, log] = createIncrement();
increment();
increment();
increment();
log();
实现私有变量
function createStack() {
return {
items: [],
push(item) {
this.items.push(item);
console.log(this.items);
},
};
}
const stack = {
items: [],
push: function () {},
};
// 对外不暴露变量
function createStack2() {
const items = [];
return {
push(item) {
items.push(item);
console.log(items);
},
};
}
let fn = createStack2();
fn.push(1);
fn.push(2);
this 的指向
普通函数中的 this
this 的概念是:this 是 JavaScript 的一个关键字,他是指函数**【执行过程】**中,自动生成的一个内部对象,是指 当前的对象,只在当前函数内部使用。
this 对象是在【运行时】基于函数的执行环境绑定的:在全局函数中,this 指向的是 Window;当函数被作为某个对象的方法调用时,this 就等于那个对象。
箭头函数的 this
箭头函数的 this 定义:箭头函数的 this 是在【定义函数时】绑定的,不是在执行过程中绑定的。简单的说,函数在定义时,this 就继承了定义函数的对象。
一共有 5 种场景。
场景 1:函数直接调用时(函数表达式、匿名函数、嵌套函数)
- 自然【执行时】,就是全局或者
undefined(严格模式下);
// 函数表达式
function myfunc() {
console.log(this); // this 是全局(window)
}
// 自然执行时,就是全局或者 undefined(严格模式下)
myfunc();
// 匿名函数
(function () {
console.log(this); // this 是全局(window)
})();
// 函数表达式
function myfunc() {
'use strict'; // 严格模式下 this 是 undefined
console.log(this); // undefined
}
// 自然执行时,就是全局或者 undefined(严格模式下)
myfunc();
// 匿名函数
(function () {
'use strict'; // 严格模式下 this 是 undefined
console.log(this); // undefined
})();
场景 2:函数被别人调用时
- 【执行时】,被别人调用时,
this就是调动的对象;
function myfunc() {
console.log(this); // this 是对象 a
}
var a = {
name: 'a',
myfunc: myfunc,
};
// 执行时,被别人调用时,this 就是调动的对象
a.myfunc();
function fn() {
console.log('隐式绑定:', this.a); // 隐式绑定: 1
}
const obj = {
a: 1,
fn,
};
obj.fn = fn;
obj.fn();
面试题:
const foo = {
bar: 10,
fn: function () {
console.log(this.bar); // undefined
console.log(this); // 全局(window / global)
},
};
// 取出
let fn1 = foo.fn;
// 执行
fn1(); // 函数直接调用,this 指向全局
追问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
追问:现在我要将 console.log('o2fn', o2.fn()) 的结果是 o2
// 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()); // o2
// this 指向最后调用他的对象,在 fn 执行时,o1.fn 抢过来挂载在自己 o2fn 上即可
场景 3:new 一个实例时
- 【执行时】,
new的时候,this就是new出来的对象;
function Person(name) {
this.name = name;
console.log(this); // this 是指实例 p
}
// 执行时,new 的时候,this 就是 new 出来的对象
var p = new Person('zhaowa');
console.log(p.name); // zhaowa
function getColor(color) {
this.color = color;
console.log(this); // Window
}
function Car(name, color) {
console.log(this); // this 指的是实例 car
this.name = name;
getColor(color); // 直接调用,getColor 方法中的 this 指向全局 Window
}
var car = new Car('卡车', '绿色');
console.log(car.name, car.color);
/*
输出:卡车 undefined
*/
面试题:
class Course {
constructor(name) {
this.name = name;
console.log('构造函数中的this >>>> ', this); // Course { name: 'course this' }
}
test() {
console.log('类方法中的this >>>> ', this); // Course { name: 'course this' }
}
}
const course = new Course('course this');
course.test();
追问:类中异步方法,this 有区别吗?
class Course {
constructor(name) {
this.name = name;
console.log('构造函数中的this >>>> ', this); // Course { name: 'course this' }
}
test() {
console.log('类方法中的this >>>> ', this); // Course { name: 'course this' }
}
asyncTest() {
console.log('异步方法外 >>>> ', this); // Course { name: 'course this' }
setTimeout(function () {
console.log('异步方法内 >>>> ', this); // 全局 window
}, 100);
}
}
const course = new Course('course this');
course.test();
course.asyncTest();
场景 4:apply/call/bind 时
this指向绑定的;
function getColor(color) {
this.color = color;
console.log(this); // this 指的是实例 car
}
function Car(name, color) {
console.log(this); // this 指的是实例 car
this.name = name;
getColor.call(this, color); // 使用 call后,getColor 方法中的 this 指向 car
}
var car = new Car('卡车', '绿色');
console.log(car.name, car.color);
/*
输出:卡车 绿色
*/
面试题:手写 call、apply、bind
- 实现
bind:
/**
* 在不考虑 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);
};
};
/**
* 如果考虑到 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;
};
/**
* 实现一个标准的 es5-shim 版 bind
*/
function bind(that) {
var target = this;
if (!isCallable(target)) {
throw new TypeError('Function.prototype.bind called on incompatible ' + target);
}
var args = array_slice.call(arguments, 1);
var bound;
var binder = function () {
if (this instanceof bound) {
var result = target.apply(this, array_concat.call(args, array_slice.call(arguments)));
if ($Object(result) === result) {
return result;
}
return this;
} else {
return target.apply(that, array_concat.call(args, array_slice.call(arguments)));
}
};
var boundLength = max(0, target.length - args.length);
var boundArgs = [];
for (var i = 0; i < boundLength; i++) {
array_push.call(boundArgs, '$' + i);
}
bound = Function(
'binder',
'return function (' + boundArgs.join(',') + '){ return binder.apply(this, arguments); }'
)(binder);
if (target.prototype) {
Empty.prototype = target.prototype;
bound.prototype = new Empty();
Empty.prototype = null;
}
return bound;
}
- 实现
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);
}
}
场景 5:箭头函数时
- 【定义时】离我最近的非箭头函数的上下文是啥,
this就是啥;
// 复习⼀下场景1
var name = 'tom';
var a = {
name: 'jack',
myfunc: function () {
setTimeout(function () {
console.log(this); // this 是 window
console.log(this.name); // tom
}, 0);
},
};
a.myfunc();
// 稍微改变⼀下
var name = 'tom';
var b = {
name: 'jack',
myfunc: function () {
var that = this;
setTimeout(function () {
console.log(this); // Window
console.log(that); // this 是 b
console.log(that.name); // jack
}, 0);
},
};
b.myfunc();
// 箭头函数
var name = 'tom';
var c = {
name: 'jack',
myfunc: function () {
setTimeout(() => {
console.log(this); // this 是 c
console.log(this.name); // jack
}, 0);
},
};
c.myfunc();
// 箭头函数
var name = 'tom';
var d = {
name: 'jack',
myfunc: () => {
setTimeout(() => {
console.log(this); // this 是 Window
console.log(this.name); // tom
}, 0);
},
};
d.myfunc();
function buybuybuyOuter() {
console.log('this-outer =========', this); // wife
const buybuybuy = () => {
console.log('this ======== ', this); // wife
};
buybuybuy();
}
var wife = {
name: 'wife',
};
wife.buybuybuyOuter = buybuybuyOuter;
wife.buybuybuyOuter();
function buybuybuyOuter() {
console.log('this-outer =========', this); // wife
const buybuybuy = () => {
console.log('this ======== ', this); // wife
};
function innerCall() {
console.log('innerCall ======== ', this); // window
buybuybuy();
}
innerCall();
}
var wife = {
name: 'wife',
};
wife.buybuybuyOuter = buybuybuyOuter;
wife.buybuybuyOuter();
var wife = {
name: 'wife',
};
const buybuybuy = () => {
console.log('this ======== ', this); // window
};
// call 和 apply 可以执行,但是无效
buybuybuy.call(wife);
总结⼀下:
- 对于直接调用的函数来说,不管函数被放在什么了地方,
this都是window; - 对于被别人调用的函数来说,被谁点出来的,
this就是谁; - 在构造函数中,类中(函数体中)出现的
this.xxx = xxx;中的this是当前类的一个实例; call/apply时,this是第一个参数;bind要优先于call/apply,call参数多,apply参数少;- 箭头函数没有自己的
this,需要看其外层的是否有函数,如果有,外层函数的this就是内部箭头函数的this,如果没有,则this是window;
相关面试题
考察 this 三板斧
function show1() {
console.log('this1 >>> ', this); // obj1
}
var obj1 = {
show1: show1,
};
obj1.show1();
function show2() {
console.log('this2 >>> ', this); // Window
}
var obj2 = {
show2: function () {
console.log('this3 >>> ', this); // obj2
show2();
},
};
obj2.show2();
var obj = {
myName: 'jack',
show: function () {
console.log('this >>> ', this);
console.log(this.myName);
},
};
var func = obj.show;
func(); // this 指向 Window,this.myName 输出 undefined
obj.show(); // this 指向 obj,this.myName 输出 jack
// 逗号表达式
/*
(0, obj.show)();
==> 等同于
var func = obj.show;
func();
*/
var name = 'tom';
var obj = {
name: 'obj',
sub: {
name: 'sub',
show: function () {
console.log('this >>> ', this);
console.log(this.name); // sub
},
},
};
obj.sub.show(); // this 指向 sub
var name = 'tom';
var obj = {
name: 'obj',
sub: {
name: 'sub',
show: () => {
console.log('this >>> ', this);
console.log(this.name); // tom
},
},
};
obj.sub.show(); // this 指向 Window
// addEventListener 中的 this
let Person = {
test: function () {
console.log(this);
},
};
let func = Person.test;
let app = document.getElementsByClassName('app');
app[0].addEventListener('scroll', Person.test); // this 指向监听的 div
window.addEventListener('scroll', Person.test); // this 指向 window
document.addEventListener('scroll', Person.test); // this 指向 document
app[0].addEventListener('scroll', function () {
console.log('this ===== ', this); // this 指向监听的 div
func(); // this 指向 window
});
app[0].addEventListener('scroll', () => {
console.log('this ===== ', this); // this 指向监听的 window
func(); // this 指向 window
});
作用域
// 提升之前
var person = 1;
function showPerson() {
console.log(person); // ƒ person() {}
var person = 2;
function person() {}
console.log(person); // 2
}
showPerson();
// 提升之后
var person = 1;
function showPerson() {
function person() {}
var person;
console.log(person); // ƒ person() {}
person = 2;
console.log(person); // 2
}
showPerson();
// 提升之前
var person = 1;
function showPerson() {
console.log(person); // ƒ person() {}
function person() {}
var person = 2;
console.log(person); // 2
}
showPerson();
// 提升之后
var person = 1;
function showPerson() {
function person() {}
var person;
console.log(person); // ƒ person() {}
person = 2;
console.log(person); // 2
}
showPerson();
var person = 1;
function showPerson() {
console.log(person); // ƒ person() {}
var person = 2;
function person() {}
}
showPerson();
var person = 1;
function showPerson() {
console.log(person); // ƒ person() {}
function person() {}
var person = 2;
}
showPerson();
for (var i = 0; i < 10; i++) {
console.log(i);
}
/*
输出:0 ~ 9
*/
for (var i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}
/*
输出:10 个 10
*/
for (var i = 0; i < 10; i++) {
(function (i) {
setTimeout(function () {
console.log(i);
}, 0);
})(i);
}
/*
输出:0 ~ 9
*/
for (let i = 0; i < 10; i++) {
console.log(i);
}
/*
输出:0 ~ 9
*/
补充:函数和变量的提升顺序!!!
函数提升要比变量提升的优先级要高一些,且不会被变量声明覆盖,但是会被变量赋值之后覆盖。
/*
提升之前
*/
console.log(a); // f a() {console.log(10)}
console.log(a()); // undefined
var a = 3;
function a() {
console.log(10); // 10
}
console.log(a); // 3
a = 6;
console.log(a()); // Uncaught TypeError: a is not a function
/*
提升之后
*/
function a() {
console.log(10);
}
var a;
console.log(a); // 变量提升后,a 的值不会被覆盖掉,所以输出:f a() {console.log(10)}
console.log(a()); // 10 undefined
a = 3;
console.log(a); // 重新赋值后,变量被覆盖,所以输出:3
a = 6;
console.log(a()); // Uncaught TypeError: a is not a function
/*
输出结果:
f a() {console.log(10)}
10
undefined
3
Uncaught TypeError: a is not a function
*/
面向对象
function Person() {
this.name = 1;
return {}; // 返回一个对象,new 出来的实例指向此对象
}
var person = new Person();
console.log(person); // {}
console.log('name >>> ', person.name); // undefined
function Person() {
this.name = 'jack';
}
Person.prototype = {
show: function () {
console.log(this); // 实例 Person
console.log('name is >>> ', this.name); // name is >>> jack
},
};
var person = new Person();
person.show();
function Person() {
this.name = 'jack';
}
Person.prototype = {
name: 'tom',
show: function () {
console.log('name is >>> ', this.name);
},
};
var person = new Person();
Person.prototype.show = function () {
console.log('new show');
};
person.show(); // new show
function Person() {
this.name = 'jack';
}
Person.prototype = {
name: 'tom',
show: function () {
console.log('name is >>> ', this.name);
},
};
var person = new Person();
var person2 = new Person();
person.show = function () {
console.log('new show');
};
person2.show(); // name is >>> jack
person.show(); // new show
综合题目
function Person() {
this.name = 'jack';
}
Person.prototype = {
name: 'tom',
show: function () {
console.log('name is >>> ', this.name);
},
};
Person.prototype.show(); // name is >>> tom
new Person().show(); // name is >>> jack