执行上下文
从程序的角度出发,JS代码被解析和执行的环境
执行上下文分类
全局执行上下文,浏览器中一般指向window,node环境中是global
函数执行上下文,函数被调用的时候动态创建
eval函数执行上下文
执行的环境分类
环境中的this
变量环境 var
词法环境 let const
外部环境
执行栈
也叫调用栈,后进先出的顺序,用来存储函数调用时所需要的所有执行上下文;最大的调用栈大概是10465;所以递归可能会出现爆栈的问题
function outer(params) {
console.log('outer');
inner();
}
function inner(params) {
console.log('inner')
}
outer()
outer函数执行时,先是全局上下文,然后outer上下文进入执行栈,接着inner上下文进栈;执行完一个调用栈之后就会该上下文就会出栈
作用域
一个独立的区域,用于隔离变量,在创建函数的时候就确定了;作用域包括全局作用域,函数作用域以及块级作用域
const name = 'globalName';
function getName() {
const name = 'getName';
console.log('getName:',name);
}
{console.log('块级作用域name:',name)}
console.log('全局作用域name:',name)
getName()
作用域链
作用域可以根据代码层次分层,子作用域可以访问父级作用域,父作用域不能访问子作用域
查找一个变量时,如果在当前作用域未找到,会到父级作用域查找,如果一直没有找到,最终将找到全局作用域,如果全局作用域也没有那么返回null;这个查找的过程就作用域链的查找
作用域和执行上下文对比
创建时间
作用域是在函数创建时就已经确定,函数内部不能访问函数外部
执行上下文是函数在执行时动态创建,执行完毕就会推出作用域栈
运行机制
作用域是静态的,在词法分析阶段就已经完成
执行上下文是动态的,执行时可能会变化
变量提升
变量可以先访问再定义,先访问时返回undefined,这被称为变量提升
注意:函数声明和变量声明以同一名字存在时,函数声明生效并且可以直接拿到函数的值
console.log(name)
var name = 'globalName';
function name() {
return 'name';
}
const和let不存在变量提升;存在暂时性死区,let和const变量在显示赋值之前不能对变量进行读写
闭包
函数内部访问了上层作用域链中的变量;详细资料juejin.cn/post/684490…
for(var i=0;i<5;i++){
setTimeout(() => {
console.log(i)
}, 100);
}
setTimeout的函数作用域访问了外部作用域的i,所以是闭包,并且输出的都是5
// 修改,或者使用let
for(var i=0;i<5;i++){
setTimeout((i) => {
console.log(i)
}, 100,i);
}
IIFE
立即调用函数表达式,避免向全局变量中添加函数和变量;使用闭包的机制将需要的参数保存起来
(function (params) {
console.log(params)
})(1) // 1
(function (params) {
console.log(params)
}(1)) // 1
+ function(params) {
console.log(params)
}(1) // 1
// 使用一元运算符强制函数运算,比如使用void、-等都是可以的
函数的一些属性
name
function sum(num1,num2) {
return num1 + num2;
}
console.log('functionName: ' , sum.name) // functionName: sum
匿名函数的name
const person = {
name:"John"
}
person.getName = function() {
return this.name;
}
console.log(person.getName.name) // 空串
const person = {
name:"John",
getName:function() {
return this.name;
}
}
console.log(person.getName.name) // getName
const person = {
name:"John",
getName:function getNameMethod() {
return this.name;
}
}
console.log(person.getName.name) // getNameMethod
new Function
const addFn = new Function('num1','num2','return num1 + num2');
console.log(addFn.name) // anonymous
bind之后
function getNameMethod() {
return this.name;
}
const person2 = {
name:'tom'
}
const bGetname = getNameMethod.bind(person2)
console.log(bGetname.name) // bound getNameMethod
get和set
const person = {
_name:'tom',
get name() {
return this._name;
},
set name(val) {
this._name = val;
}
}
person.name = 'Jhon'
console.log(person.name) // Jhon
const discriptor = Object.getOwnPropertyDescriptor(person,'name')
console.log(discriptor)
length
函数对象的一个属性值,指该函数有多少个必须要传入的参数,即形参的个数;arguments.length代表的是实参
特点
不包含剩余参数
不包含有默认值的参数
仅包含第一个具有默认值之前的参数
bind之后的length为length-bind的参数个数,最少为0
// ...args是剩余参数
function sum(num1,num2,...args) {
console.log(...args)
return num1 + num2;
}
console.log(sum.length) // 2
function sum(num1 = 1,num2,...args) {
console.log(...args)
return num1 + num2;
}
console.log(sum.length) // 0
caller
谁调用了函数,可以用于收集调用栈信息或者检查调用环境;该特性是非标准特性,尽量不要使用于生产环境;函数在全局作用域被调用返回null;函数内部作用域调用,指向调用它的函数
function getStack(fn) {
const stacks = [];
let caller = fn.caller;
while (caller) {
stacks.unshift(caller.name);
caller = caller.caller;
}
return stacks
}
function a() {
console.log("a");
const stacks = getStack(a);
console.log('stacks: ' , stacks)
}
function b() {
a();
console.log('b')
}
function c() {
b();
console.log('c')
}
c();
argument.callee
包含正在执行的函数,起源于匿名函数的递归
function sumTotal(n) {
if(n === 1){
return 1;
}
return sumTotal(n-1) + n;
}
console.log([5,10].map(sumTotal)) // [15,55]
const res = [5,10].map(function (n) {
if(n === 1){
return 1;
}
// 使用arguments.callee会影响this
return arguments.callee(n-1) + n;
})
console.log(res) // [15,55]
严格模式下:caller、callee、arguments都是不可用的
this
执行上下文的一个属性,在非严格模式下总是指向一个对象,在严格模式下可以是任意值
绑定规则
默认绑定
严格模式下,浏览器和nodejs都指向undefined
非严格模式下,浏览器指向window,nodejs指向global
单独打印浏览器this指向的是window,nodejs下是{}
const name = "tom"
function getName() {
console.log(this)
return this.name
}
getName()
console.log(this)
隐式绑定
作为某个对象的属性被调用的时候
const name = "tom"
function getName() {
console.log(this)
return this.name
}
const person = {
name:"person的name",
getName
}
person.getName()
其他的隐式绑定
const request = new XMLHttpRequest();
request.open('GET','./');
request.send();
request.onloadend = function () {
console.log(this)
}
btn.addEventListener("click", function(){
console.log(this)
});
显示绑定
显示的表达this的指向;非严格模式下,传入null和undefined默认指向window
Function.prototype.call
const obj = {name:"tom"}
function getName() {
console.log(this.name)
}
getName.call(obj) // this指向obj打印tom
Function.prototype.apply
和call一样,只是传参数不一样,apply后面的参数是以数组形式传入,call是挨个传入
Function.prototype.bind
bind返回的是函数,需要调用;bind可以进行多次邦迪,只有第一个生效
const obj = {name:"tom"}
function getName() {
console.log(this.name)
}
const bindName = getName.bind(obj);
bindName()
function add(num1, num2, num3, num4) {
return num1 + num2 + num3 + num4;
}
const add2 = add.bind(null,10,20);
console.log(add2(30,40))
属性绑定符
function getName() {
console.log(this.name)
}
({name:"tom"})::getName();
// 等同于getName.call({name:"tom"})
new
实例化一个函数或者class
function Person() {
this.name = name;
this.getName = function() {
return this.name;
}
}
const person = new Person('tom');
// 这里的getName绑定到了person对象上
console.log(person.getName())
对于Function、return会影响返回值
return非对象,实际返回系统内部的对象;return对象,实际返回该对象
function MyObj() {
this.name = "MyObj";
}
function MyObj1() {
this.name = "MyObj";
return {
name:"MyObj1"
}
}
function MyObj2() {
this.name = "MyObj2";
return undefined
}
console.log(new MyObj())
console.log(new MyObj1())
console.log(new MyObj2())
手写new
1、创建一个空对象
2、设置空对象的原型
3、执行构造函数方法,并把相关的属性方法添加到对象上
4、返回对象,如果构造函数返回的值是对象则返回,否则返回第一步创建的对象
const slice = Array.prototype.slice;
function newObj(constructor) {
const args = slice.call(arguments,1);
const obj = {};
obj.__proto__ = constructor.prototype;
const res = constructor.apply(obj,args);
return res instanceof Object ? res : obj;
}
箭头函数
特点:
1、简洁
2、没有自己的this,arguments、super、new.target
3、适合需要匿名函数的地方
4、不能用于构造函数
5、箭头函数中的this指向上层作用域第一个非箭头函数的this
// 用let、const不会挂载到window
var name = "window的name";
const getName = ()=>this.name;
console.log(getName()) // window的name
const person = {
name:'person的name',
getName : ()=>this.name
}
console.log(person.getName()) // window的name
const person2 = {
name:'person2的name',
getPerson(){
return {
getName : ()=>this.name
}
}
}
console.log(person2.getPerson().getName()) // person2的name
var name = "window的name";
const person = {
name:'person的name',
getName(){
return ()=>this.name
}
}
console.log(person.getName()()) // person的name
console.log(person.getName.call({name:"name"})()) // name
手写call
思路:某个方法进行call调用时,等同于把方法作为call的第一个参数的某个属性并调用
注意事项:
1、第一个参数的某个属性,该属性应该确保不是该参数之前的属性,如果是call函数调用完之后要还原该属性值
2、手写的时候尽量不要使用ES6
3、call方法不止是在浏览器调用,也可能在node等其他环境调用
4、在严格模式下,是找不到window的,如果传入null将会报错
5、应该只有函数能调用call
// 判断是什么环境
const getGlobal = function () {
if(typeof self !=="undefined"){return self};
if(typeof window !=="undefined"){return window};
if(typeof global !=="undefined"){return global};
throw new Error('unable to lacate global object')
}
// 是否支持严格模式
const hasStrictMode = (function(){
"use strict";
return this == undefined;
}())
// 是否使用了严格模式
const isStrictMode = (function(){
return this == undefined
}())
// 是否是函数调用
function isFunction(fn) {
return typeof fn === 'function' || Object.prototype.toString.call(fn) === '[object FUnction]';
}
function getContext(context) {
// 没有严格模式,或者有严格模式但是不处于严格模式
if(!hasStrictMode || (hasStrictMode && !isStrictMode)){
return (context == null || context == void 0) ? getGlobal() : Object(context);
}
// 严格模式下,包装上下文
return Object(context);
}
Function.prototype.call = function(context){
if(!isFunction(this)){
throw new TypeError(this+' is not a function')
}
const ctx = getContext(context);
// 创建唯一id
const name = "__fn__"+Math.random()+"_"+new Date().getTime();
// 判断是否有该属性,如果有先存下来
let originVal;
const hasOriginVal = isFunction(ctx.hasOwnProperty) ? ctx.hasOwnProperty(name) : false;
if(hasOriginVal) {
originVal = ctx[name];
}
ctx[name] = this;
const arr = [];
for(let i=0;i<arguments.length;i++){
// 这里需要使用字符串
arr.push('arguments['+i+']');
}
const r = eval('ctx['+name+']('+arr.join('')+')');
if(hasOriginVal){
ctx[name] = originVal;
}else {
delete context[name];
}
return r;
}
// 使用new Function代替eval
function creatFun(argsLength) {
// return ctx[name](arg1,arg2,...)
let code = 'return ctx[name](';
for(let i=0;i<argsLength;i++){
if(i>0){
code += ',';
}
code+='args['+ i +']';
}
code += ')';
return new Function('ctx','name','args',code)
}
const obj = {
log(...args){
console.log(...args);
}
}
function log(...args) {
creatFun(args.length)(obj,"log",[...args])
}
console.log(log)
log(1,2,3)
简易版
Function.prototype.call = function(context){
context = context || window;
context["fn"] = this;
let arg = [...arguments].slice(1);
const r = context["fn"](...arg);
delete context["fn"];
return r;
}
eval
将传入的字符串当作JS代码解析并运行
console.log(eval('2+2')) // 4
使用场景
系统内部的setTimeout或者setInterval;当传入字符串时其内部使用的就是eval
setTimeout('console.log(Date.now())', 1000);
setInterval('console.log(Date.now())', 1000);
JSON字符串转对象
const jsonS = '{a:1,b:2}';
const obj = eval('('+jsonS+')')
console.log(obj)
动态生成函数或者变量
const sumAdd = eval(`(function add(num1,num2){return num1 + num2})`);
console.log(sumAdd(1,2)) // 3
其他例子
const arr = [1,2,3,4];
const res = eval(arr.join('+'));
console.log(res) // 10
const globalThis = (function() {return(void 0,eval)("this")})()
console.log(globalThis) // 浏览器中获取了window
注意事项
1、 动态脚本的安全性,可以使用CSP来阻止eval运行
2、调试困难,可以在字符串中加debugger
3、性能比较低,eval会创建新的作用域,也会保存全部的执行上下文,要使用eval可以提前构造函数
4、不好掌控
var name = "全局的name";
function test() {
const name = "test的name";
console.log(eval('name')) // test的name
console.log(window.eval('name')) // 全局的name
}
test()
直接调用:eval、(eval)、eval = window.eval(变量不能改名)、{eval} = window、with({eval})
new Function
创建一个新的Function;语法:new Function([arg1[,arg2[,...argN]],],functionBody)
基本使用:new Function('a','b','return a+b')(10,20) // 30
注意事项
1、基于全局环境创建,不能访问当前环境的变量
var name = "全局的name";
const fnStr = `console.log(name)`
const obj = {
test(){
const name = "test的name";
const fn = new Function(fnStr);
fn();
},
testEval(){
const name = "testEval的name";
const fn = eval(`(function(){${fnStr}})`);
fn();
}
}
obj.test() // 全局的name
obj.testEval() // testEval的name
2、作为对象属性调用的时候能访问对象的属性
var name = "全局的name";
const fn = new Function(`console.log(this.name)`)
const obj = {
name:'obj的name',
fn
}
fn() // 全局的name
obj.fn() // obj的name
应用
1、获取全局对象
const fn = new Function(`return this`)
fn()
2、模板解析
<div id="template">
<div>名字:${name}</div>
<div>年龄:${age}</div>
</div>
function parse(source,data) {
return new Function('data', `
with(data){
return \`${source}\`
}
`)(data)
}
const result = parse(template.innerHTML,{
name:'Jhon',
age:18
})
template.innerHTML = result
链式调用
本质:返回对象本身或者返回同类型的实例对象
优点:可读性强,语义好理解,代码简洁,易于维护等
缺点:调试不方便,需要开发人员的抽象能力强
使用场景:需要多次计算或者赋值进行加工,逻辑上有特定的顺序比如promise
返回对象实例本身
class Calculator {
constructor(val){
this.val = val;
}
double(){
this.val = this.val * 2;
return this;
}
add(num) {
this.val = this.val + num;
return this;
}
minus(num) {
this.val = this.val - num;
return this;
}
multi(num) {
this.val = this.val * num;
return this;
}
divide(num) {
this.val = this.val / num;
return this;
}
pow(num) {
this.val = Math.pow(this.val,num);
return this;
}
get value(){
return this.val;
}
}
const cal = new Calculator(10);
const val = cal.add(10).pow(2).value;
console.log(val) // 400
返回新的实例
class Calculator {
constructor(val){
this.val = val;
}
double(){
const val = this.val * 2;
return new Calculator(val);
}
add(num) {
const val = this.val + num;
return new Calculator(val);
}
minus(num) {
const val = this.val - num;
return new Calculator(val);
}
multi(num) {
const val = this.val * num;
return new Calculator(val);
}
divide(num) {
const val = this.val / num;
return new Calculator(val);
}
pow(num) {
const val = Math.pow(this.val,num);
return new Calculator(val);
}
get value(){
return this.val;
}
}
const cal = new Calculator(10);
const val = cal.add(10).pow(2).value;
console.log(val) // 400