JS知识点回顾——函数

91 阅读9分钟

执行上下文

从程序的角度出发,JS代码被解析和执行的环境

执行上下文分类

全局执行上下文,浏览器中一般指向window,node环境中是global

函数执行上下文,函数被调用的时候动态创建

eval函数执行上下文

执行的环境分类

环境中的this

变量环境 var

词法环境 let const

外部环境

image.png

执行栈

也叫调用栈,后进先出的顺序,用来存储函数调用时所需要的所有执行上下文;最大的调用栈大概是10465;所以递归可能会出现爆栈的问题

function outer(params) {
    console.log('outer');
    inner();
}
function inner(params) {
    console.log('inner')
}
outer()

outer函数执行时,先是全局上下文,然后outer上下文进入执行栈,接着inner上下文进栈;执行完一个调用栈之后就会该上下文就会出栈

作用域

一个独立的区域,用于隔离变量,在创建函数的时候就确定了;作用域包括全局作用域,函数作用域以及块级作用域

image.png

const name = 'globalName';
function getName() {
    const name = 'getName';
    console.log('getName:',name);
}
{console.log('块级作用域name:',name)}
console.log('全局作用域name:',name)
getName()

image.png

作用域链

作用域可以根据代码层次分层,子作用域可以访问父级作用域,父作用域不能访问子作用域

查找一个变量时,如果在当前作用域未找到,会到父级作用域查找,如果一直没有找到,最终将找到全局作用域,如果全局作用域也没有那么返回null;这个查找的过程就作用域链的查找

作用域和执行上下文对比

创建时间

作用域是在函数创建时就已经确定,函数内部不能访问函数外部

执行上下文是函数在执行时动态创建,执行完毕就会推出作用域栈

运行机制

作用域是静态的,在词法分析阶段就已经完成

执行上下文是动态的,执行时可能会变化

变量提升

变量可以先访问再定义,先访问时返回undefined,这被称为变量提升

注意:函数声明和变量声明以同一名字存在时,函数声明生效并且可以直接拿到函数的值

console.log(name)
var name = 'globalName';
function name() {
    return 'name';
}

image.png

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)

image.png

image.png

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();

image.png

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)

image.png

image.png

隐式绑定

作为某个对象的属性被调用的时候

const name = "tom"
function getName() {
  console.log(this)
  return this.name
}
const person = {
      name:"person的name",
      getName
}
person.getName()

image.png

其他的隐式绑定

const request = new XMLHttpRequest();
request.open('GET','./');
request.send();
request.onloadend = function () {
      console.log(this)
}

image.png

btn.addEventListener("click", function(){
      console.log(this)
});

image.png

显示绑定

显示的表达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())

image.png

手写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)

image.png

动态生成函数或者变量
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运行

image.png

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})

image.png

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

image.png

链式调用

本质:返回对象本身或者返回同类型的实例对象

优点:可读性强,语义好理解,代码简洁,易于维护等

缺点:调试不方便,需要开发人员的抽象能力强

使用场景:需要多次计算或者赋值进行加工,逻辑上有特定的顺序比如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