1. 执行上下文
JS代码运行的时候都在执行上下文中运行。
类型
-
全局执行上下文
- 创建一个全局的 window 对象(浏览器的情况下)
- 设置 this 的值等于这个全局对象
-
函数执行上下文
- 每个函数都有它自己的执行上下文,不过是在函数被调用时创建的
-
Eval函数执行上下文
- 执行在 eval 函数内部的代码也会有它属于自己的执行上下文
执行栈(调用栈 LIFO)
用来存储代码运行时创建的所有执行上下文。
当 JavaScript 引擎第一次遇到脚本 ----->
创建一个全局的执行上下文并且压入当前执行栈 ----->
当引擎遇到一个函数调用 -----> 它会为该函数创建一个新的执行上下文并压入栈的顶部 ----->
该函数执行结束时 -----> 执行上下文从栈中弹出 ----->
控制流程到达当前栈中的下一个上下文
每个执行上下文有3个重要属性
-
变量对象(Variable object,VO)
-
存储了在上下文中定义的变量和函数声明
-
全局上下文下的变量对象
- 全局对象
-
函数上下文下的变量对象
- 分析 AO
- 执行
-
-
-
作用域链(Scope chain)
-
this
变量对象
变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。
全局上下文中的变量对象就是全局对象!!!
函数上下文下的变量对象
定义(此处可以忽略)
活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object 呐,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。
活动对象何时创建(活动对象其实就是变量对象)
活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。
执行过程
执行上下文的代码会分成两个阶段进行处理:分析和执行,我们也可以叫做:
- 进入执行上下文
- 代码执行
1. 进入执行上下文
变量对象会包括:
-
函数的所有形参 (如果是函数上下文)
- 由名称和对应值组成的一个变量对象的属性被创建
- 没有实参,属性值设为 undefined
-
函数声明
- 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
- 如果变量对象已经存在相同名称的属性,则完全替换这个属性
-
变量声明
- 由名称和对应值(undefined)组成一个变量对象的属性被创建;
- 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1);
在进入执行上下文后,这时候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
2. 代码执行
在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值 还是上面的例子,当代码执行完后,这时候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
总结:变量对象的创建过程
- 全局上下文的变量对象初始化是全局对象
- 函数上下文的变量对象初始化只包括 Arguments 对象
- 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
- 在代码执行阶段,会再次修改变量对象的属性值
2. 作用域
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。
3. 作用域链
函数的作用域在函数定义的时候就决定了。
函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!
当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。
function foo() {
function bar() {
...
}
}
函数创建时,各自的[[scope]]为:
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
];
当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。
这时候执行上下文的作用域链,我们命名为 Scope:
Scope = [AO].concat([[Scope]]);
至此,作用域链创建完毕。
4. 闭包
定义
闭包是指那些能够访问自由变量的函数。
自由变量:在函数中使用的既不是函数参数也不是函数局部变量的变量。
- 理论上来讲:所有的函数都有可能是闭包,函数中去访问全局变量相当于是在访问自由变量。
- 实践角度: 2.1 即使创建它的上下文已经被销毁了,它仍然存在(内部函数从父函数中返回) 2.2 代码中引用了自由变量
应用场景
4.1 柯里化函数
避免频繁调用具有相同参数的函数,同时能够轻松的复用。 其实就是封装一个高阶函数(返回值是函数的函数)。
function getArea(width, height) {
return width + height
}
const area1 = getArea(10, 20)
const area2 = getArea(10, 30)
const area3 = getArea(10, 40)
const area4 = getArea(10, 50)
function getArea(width) {
return height => {
return width + height
}
}
const getAreaWidth = getArea(10)
const area1 = getAreaWidth(20)
const area2 = getAreaWidth(30)
const area3 = getAreaWidth(40)
const area4 = getAreaWidth(50)
4.2 使用闭包实现私有方法/变量
模块化:现代化的打包方式,最终就是每个模块的代码都是相互独立的。
function funOne(i) {
function funTwo() {
console.log('数字', i)
}
return funTwo
}
const fa = funOne(110) // 110
const fb = funOne(111) // 111
const fc = funOne(112) // 112
4.3 匿名自执行函数
var funOne = (function() {
var num = 0
return function() {
num ++
return num
}
})()
console.log(funOne()) // 1
console.log(funOne()) // 2
console.log(funOne()) // 3
4.4 缓存一些结果
外部函数中创建一个数组,闭包函数可以获取或者修改这个数组的值,延长了变量的生命周期。
function parent() {
let arr = []
function child(i) {
arr.push(i)
console.log(arr.join())
}
return child
}
const fn = parent()
fn(1) // 1
fn(2) // 1,2
好处
- 避免污染全局变量
总结
-
创建私有变量;
-
延长变量的生命周期;
-
缺点:
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(
object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
面试高频题
**1. 实现一个 compose 函数
function fn1(x) {
return x + 1
}
function fn2(x) {
return x + 2
}
function fn3(x) {
return x + 3
}
function fn4(x) {
return x + 4
}
const a = compose(fn1, fn2, fn3, fn4)
console.log(a(1)) // 1 + 4 + 3 + 2 + 1 = 11
// 实现
function compose() {
}
实现
// 实现
function compose() {
const argsFnList = [...arguments]
return (arg) => argsFnList.reduce((pre, cur) => cur(pre), arg)
}
2. 柯里化
function currying(fn, ...args) {
}
const add = (a, b, c) => a + b + c
const a1 = currying(add, 1)
const a2 = a1(2)
console.log(a2(3)) // 1 + 2 + 3 = 6
实现
function currying(fn, ...args) {
const originLength = fn.length
let allArgs = [...args]
const resFn = (...newArgs) => {
allArgs = [...allArgs, ...newArgs]
if (allArgs.length === originLength) {
return fn(...allArgs)
} else {
return resFn
}
}
return resFn
}
3. 实现 compose
let middlewares = []
middlewares.push((next) => {
console.log(1)
next()
console.log(6)
})
middlewares.push((next) => {
console.log(2)
next()
console.log(5)
})
middlewares.push((next) => {
console.log(3)
next()
console.log(4)
})
let fn = compose(middlewares)
fn() // 输出 1 2 3 4 5 6 洋葱模型
// 实现
function compose() {
}
实现
function compose(middlewares) {
const copyMiddlewares = [...middlewares]
let index = 0
const fn = () => {
if (index >= middlewares.length) {
return
}
const currentMiddleware = copyMiddlewares[index++]
return currentMiddleware(fn)
}
return fn
}
4. koa-compose
const compose = require('koa-compose');
function one(ctx,next){
console.log('第一个');
next(); // 控制权交到下一个中间件(实际上是可以执行下一个函数),
}
function two(ctx,next){
console.log('第二个');
next();
}
function three(ctx,next){
console.log('第三个');
next();
}
// 传入中间件函数组成的数组队列,合并成一个中间件函数
const middlewares = compose([one, two, three]);
// 执行中间件函数,函数执行后返回的是Promise对象
middlewares().then(function (){
console.log('队列执行完毕');
})
实现
function compose (middleware) {
// 如果传入的不是数组,则抛出错误
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
// 数组队列中有一项不为函数,则抛出错误
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
// compose函数调用后,返回的是以下这个匿名函数
// 匿名函数接收两个参数,第一个根据使用场景决定
// 第一次调用时候第二个参数next实际上是一个undefined,因为初次调用并不需要传入next参数
// 这个匿名函数返回一个 promise
return function(context, next) {
//初始下标为-1
let index = -1
return dispatch(0)
function dispatch (i) {
// 如果传入i为负数且<=-1 返回一个Promise.reject携带着错误信息
// 所以执行两次next会报出这个错误。将状态rejected,就是确保在一个中间件中next只调用一次
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
// 执行一遍next之后,这个index值将改变
index = i
// 根据下标取出一个中间件函数
let fn = middleware[i]
// next在这个内部中是一个局部变量,值为undefined
// 当i已经是数组的length了,说明中间件函数都执行结束,执行结束后把fn设置为undefined
// 问题:本来middleware[i]如果i为length的话取到的值已经是undefined了,为什么要重新给fn设置为undefined呢?
if (i === middleware.length) fn = next
//如果中间件遍历到最后了。那么。此时return Promise.resolve()返回一个成功状态的promise
// 方便之后做调用then
if (!fn) return Promise.resolve()
// try catch保证错误在Promise的情况下能够正常被捕获。
// 调用后依然返回一个成功的状态的Promise对象
// 用Promise包裹中间件,方便await调用
// 调用中间件函数,传入context(根据场景不同可以传入不同的值,在KOa传入的是ctx)
// 第二个参数是一个next函数,可在中间件函数中调用这个函数
// 调用next函数后,递归调用dispatch函数,目的是执行下一个中间件函数
// next函数在中间件函数调用后返回的是一个promise对象
// 读到这里不得不佩服作者的高明之处。
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
5. this
如果要判断一个函数的this绑定,就需要找到这个函数的直接调用位置。然后可以顺序按照下面四条规则来判断this的绑定对象:
- 由
new调用:绑定到新创建的对象 - 由
call或apply、bind调用:绑定到指定的对象 - 由上下文对象调用:绑定到上下文对象
- 默认:全局对象
注意:箭头函数不使用上面的绑定规则,根据外层作用域来决定this,继承外层函数调用的this绑定。
结论
在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。如果函数独立调用,那么该函数内部的this,则指向undefined。但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。
demo 分析
var a = 20;
var obj = {
a: 10,
c: this.a + 20,
fn: function () {
return this.a;
}
}
console.log(obj.c); // 40
console.log(obj.fn()); // 10
单独的{}不会形成新的作用域,因此这里的this.a,由于并没有作用域的限制,它仍然处于全局作用域之中。所以这里的this其实是指向的window对象。
var a = 20;
var foo = {
a: 10,
getA: function () {
return this.a;
}
}
console.log(foo.getA()); // 10
var test = foo.getA;
console.log(test()); // 20
foo.getA()中,getA是调用者,他不是独立调用,被对象foo所拥有,因此它的this指向了foo。而test()作为调用者,尽管他与foo.getA的引用相同,但是它是独立调用的,因此this指向undefined,在非严格模式,自动转向全局window。
var a = 20;
function getA() {
return this.a;
}
var foo = {
a: 10,
getA: getA
}
console.log(foo.getA()); // 10
function foo() {
console.log(this.a)
}
function active(fn) {
fn(); // 真实调用者,为独立调用
}
var a = 20;
var obj = {
a: 10,
getA: foo
}
active(obj.getA); // 20
6. new 过程
4个阶段
通过new操作符调用构造函数,会经历以下4个阶段:
- 创建一个新的对象;
- 将构造函数的this指向这个新对象;
- 指向构造函数的代码,为这个对象添加属性,方法等;
- 返回新对象。
详细解析
- 创建了一个全新的对象,这个对象会被执行
[[Prototype]](也就是__proto__)链接。 - 生成的新对象会绑定到函数调用的
this。 - 通过
new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。 - 如果函数没有返回对象类型
Object(包含Functoin,Array,Date,RegExg,Error),那么new表达式中的函数调用会自动返回这个新的对象。
实现 new 过程
/**
* 模拟实现 new 操作符
* @param {Function} ctor [构造函数]
* @return {Object|Function|Regex|Date|Error} [返回结果]
*/
const newOperator = (function() {
// 使用栈存储的目的:确保多次 new 时 target 取到对应的值
const _newStack = [];
function newOperator(ctor) {
// 设定new.target
newOperator.target = ctor;
// 生成新的对象,其隐式原型指向构造函数的原型对象
const obj = Object.create(ctor.prototype);
// 执行构造函数,并返回结果
const result = ctor.apply(obj, Array.prototype.slice.call(arguments, 1));
// 重置new.target
newOperator.target = null;
// 判断最终返回对象
return ((typeof result === 'object' && result !== null) || typeof result === 'function') ? result : obj;
}
// 设定target的get、set方法
Reflect.defineProperty(newOperator, 'target', {
get() {
return _newStack[_newStack.length - 1];
},
set(target) {
if (target == null) {
_newStack.pop();
} else {
_newStack.push(target);
}
}
})
return newOperator;
})();
function B() {
if (newOperator.target === B) {
console.log('new调用 B')
} else {
console.log('非new调用 B')
}
return {balabala: 123};
}
function A() {
const b = newOperator(B);
if (newOperator.target === A) {
console.log('new调用 A')
} else {
console.log('非new调用 A')
}
}
newOperator(A);
对于不支持ES5的浏览器,MDN上提供了polyfill方案
if (typeof Object.create !== "function") {
Object.create = function (proto, propertiesObject) {
if (typeof proto !== 'object' && typeof proto !== 'function') {
throw new TypeError('Object prototype may only be an Object: ' + proto);
} else if (proto === null) {
throw new Error("This browser's implementation of Object.create is a shim and doesn't support 'null' as the first argument.");
}
if (typeof propertiesObject != 'undefined') throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");
function F() {}
F.prototype = proto;
return new F();
};
}
实现 call、apply、bind
call
- 将函数设为对象的属性
- 执行该函数
- 删除该函数
Function.prototype.call2 = function(context) {
// 首先要获取调用call的函数,用this可以获取
context.fn = this;
context.fn();
delete context.fn;
}
问题一:从 Arguments 对象中取值,取出第二个到最后一个参数,然后放到一个数组里
Function.prototype.call2 = function(context) {
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
eval('context.fn(' + args +')');
delete context.fn;
}
问题二:
1.this 参数可以传 null,当为 null 的时候,视为指向 window
2.函数是可以有返回值的!
Function.prototype.call2 = function (context) {
var context = context || window;
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('context.fn(' + args +')');
delete context.fn
return result;
}
apply
apply 的实现跟 call 类似
Function.prototype.apply = function (context, arr) {
var context = Object(context) || window;
context.fn = this;
var result;
if (!arr) {
result = context.fn();
} else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fn(' + args + ')')
}
delete context.fn
return result;
}
bind
MDN polyfill
Function.prototype.bind = function (context) {
var context = context || window
var oThis = this
var args = Array.prototype.slice.call(arguments, 1)
var fNop = function(){}
var fBound = function () {
return oThis.apply(this instanceof fNop ? this : context || this,
args.concat(Array.prototype.slice.call(arguments)))
}
fNop.prototype = oThis.prototype
fBound.prototype = new fNop()
return fBound
}
总结
- 三者都是用来改变函数的
this指向 - 三者的第一个参数都是
this指向的对象 bind是返回一个绑定函数可稍后执行,call、apply是立即调用- 三者都可以给定参数传递
call给定参数需要将参数全部列出,apply给定参数数组
7. 原型
JavaScript 中万物皆对象,对象皆出自构造函数。
对象分为:函数对象和普通对象。
对象独有:__proto__ constructor;
函数独有:prototype。
JavaScript 中函数也是对象,故函数也拥有__proto__ constructor 属性。
var A = function(){}; // A是一个方法,当然也是个对象
var a = new A();
定义
给其它对象提供共享属性的对象。
prototype 自己也是对象,只是被用以承担了某个职能。
公式
实例.__proto__ === 实例的构造函数.prototype
JavaScript 中的 `Object`和`Function` 就是典型的函数对象。
所有函数对象的__proto__都指向Function.prototype。
8. 原型链
原型链的形成是靠 __proto__ 而非prototype。
instanceof
object instanceof constructor
instanceof 用来检测 contructor.prototype 是否在 object 的原型链上。
function instance_of(l, r) {
var rProto = r.prototype
var lProto = l.__proto__
console.log(typeof rProto)
console.log(typeof lProto)
if (!lProto) return false
while(l) {
console.log(typeof lProto)
if (lProto === rProto) {
return true
}
lProto = lProto.__proto__
}
}
解析
Object instanceof Object
rProto = Object.prototype
lProto = Object.__proto__ = Function.prototype
// 进入循环体,第一次循环
lProto !== rProto
lProto = lProto.__proto__ = Function.prototype.__proto__ = Object.prototype
// 第二次循环
lProto === rProto
Function instanceof Function
9. 继承
9.1 ES5 中的继承实现方式
9.1.1 new 关键字
- 创建对象 obj;
- 将对象 obj 的原型指向构造函数,使对象 obj 可以访问到构造函数原型对象的属性;
- 将构造函数的
this绑定到当前对象 obj,使对象 obj 可以访问到构造函数的属性; - 如果构造函数返回一个对象,那么我们返回这个对象,否则返回对象 obj。
以 new 操作符调用构造函数的时候,函数内部实际上发生以下变化:
- 创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
- 属性和方法被加入到 this 引用的对象中。
- 新创建的对象由 this 所引用,并且最后隐式的返回 this.
function objectFactory() {
var obj = new Object()
var Constructor = [].shift.call(arguments) // 第一个参数就是构造函数
function F(){}
F.prototype = Constructor.prototype
obj = new F()
var result = Constructor.apply(obj, arguments)
return typeof result === 'object' ? result : obj
}
9.1.2 类式继承
子元素.prototype.__proto__ = 父元素.prototype
function SuperClass() {
this.superValue = true;
}
SuperClass.prototype.getSuperValue = function() {
return this.superValue;
}
function SubClass() {
this.subValue = false;
}
SubClass.prototype = new SuperClass();
SubClass.prototype.getSubValue = function() {
return this.subValue;
}
var instance = new SubClass();
console.log(instance instanceof SuperClass) // true
console.log(instance instanceof SubClass) // true
console.log(SubClass instanceof SuperClass)// false
缺点
- 子类通过
prototype对父类实例化,继承了父类的属性和方法,如果父类中有引用类型的属性,会被共享。一个子类实例对共享属性的修改会对其他实例的属性造成影响。 - 子类的继承是靠其原型
prototype属性对父类实例进行实例化的,因此不能对父类传递参数。
9.1.3 构造函数继承
function SuperClass() {
this.superValue = true;
}
SuperClass.prototype.getSuperValue = function() {
return this.superValue;
}
function SubClass() {
SuperClass.call(this, arguments)
}
缺点
- 父类的原型方法不会被继承;
- 如果想被继承,就需要把
prototype放到构造函数中,这样创建的每个实例都会有一套单独的属性和方法,不能复用。
9.1.4 组合继承
function SuperClass() {
this.superValue = true;
}
SuperClass.prototype.getSuperValue = function() {
return this.superValue;
}
function SubClass() {
SuperClass.call(this, arguments)
}
SubClass.prototype = new SuperClass()
缺点
父类构造函数被调用2次。
9.1.5 原型式继承
function inheritObject(o) {
//声明一个过渡对象
function F() { }
//过渡对象的原型继承父对象
F.prototype = o;
//返回过渡对象的实例,该对象的原型继承了父对象
return new F();
}
缺点
- 原型式继承和类式继承一个样子,对于引用类型的变量,还是存在子类实例共享的情况。
9.1.6 寄生式继承
function CreateObj(obj) {
var o = inheritObject(obj)
o.method = function(){}
return o
}
9.1.7 寄生组合式继承
function inheritObject(o) {
//声明一个过渡对象
function F() { }
//过渡对象的原型继承父对象
F.prototype = o;
//返回过渡对象的实例,该对象的原型继承了父对象
return new F();
}
function inheritPrototype(subClass,superClass) {
// 复制一份父类的原型副本到变量中
var p = inheritObject(superClass.prototype);
// 修正因为重写子类的原型导致子类的constructor属性被修改
p.constructor = subClass;
// 设置子类原型
subClass.prototype = p;
}
ES6 中的继承-Class继承
在class 中继承主要是依靠两个东西:
-
extends -
super而且对于该继承的效果和寄生组合继承方式一样。
10. 深浅拷贝
深拷贝和浅拷贝都是针对的引用类型。
10.1 浅拷贝
浅拷贝就是只复制引用,而未复制真正的值。
JavaScript中的拷贝方法都是首层浅拷贝。concat、slice、Object.assign()、 ...展开符 都是首层浅拷贝。
10.2 深拷贝
目前实现深拷贝的方法主要是两种:
利用 JSON 对象中的 parse 和 stringify; 利用递归来实现每一层都重新创建对象并赋值。
10.2.1 JSON.parse(JSON.stringify(obj))
如果对象中含有一个函数时(很常见),就不能用这个方法进行深拷贝,因为 undefined、function、symbol 会在转换过程中被忽略。
10.2.2 递归
对每一层的数据都实现一次 创建对象->对象赋值 的操作。
function isObject(obj) { return obj && typeof obj === 'object' } function deepClone(obj) { if (!isObject(obj)) return obj let targetObj = obj.constructor === Array ? [] : {} for (let key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { if (isObject(obj[key]) { targetObj[key] = deepClone(obj[key]) } else { targetObj[key] = obj[key] } } } }总结
- 赋值运算符 = 实现的是浅拷贝,只拷贝对象的引用值;
- JavaScript 中数组和对象自带的拷贝方法都是“首层浅拷贝”;
- JSON.stringify 实现的是深拷贝,但是对目标对象有要求:非function, undefined,symbol;
- 若想真正意义上的深拷贝,用递归实现。
11. Event Loop
12. Promise