前端八股文——函数

109 阅读9分钟

作用域

概念

作用域链概念:

保证执行环境有权访问的变量和函数是有序的,只能向上访问,访问到 window 对象停止。作用域控制着变量与函数的可见性及生命周期。只引用不包含实际变量对象。

执行上下文:

概念:js执行代码时的运行环境。确定该函数执行期间用到的变量:this、变量、函数等。

  1. 全局执行上下文

  2. 函数执行上下文【函数执行的时候就会创建函数执行上下文,压入上下文执行栈,运行结束弹出】

  3. eval执行上下文(eval:将对应字符串解析成 js 代码并运行。)

在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。

this

  1. 普通函数:指向window

  2. 箭头函数:本身无this,指向调用它的普通函数。一但绑定不可更改。

  3. new:指向实例(原因见new操作符的操作)

  4. call(),apply(),bind() 可以改变this指向,取决于第一个参数,为null指向window。箭头函数使用无效。 | |call | apply | bind | | ---- | --------------------- | --------------------- | --------------------- | | | 立即改变 | 立即改变 | 下次调用后改变 | | 调用参数 | call(this, arg1,arg2) | apply(this,arguments) | bind(this, arg1,arg2) | | 性能最好 | √ |

call,apply,bind实现

实现原理:1.将函数设为对象属性;2.执行函数;3.删除该属性

// call
Function.prototype.myCall = function (context, ...args) {
    [_fn] = Symbol("fn")  // 防止覆盖原有变量
    context = context || window  // 设置对象
    context[_fn] = this  // 获取调用的函数
    let result = context[_fn](...args)  //执行
    delete context[_fn]   //删除该对象
    return result    // 返回执行结果
}

// apply
Function.prototype.myApply = function(context, args) {
    [_fn] = Symbol("fn")  // 防止覆盖原有变量
    context = context || window
    context[_fn]= this
    let result = context[_fn](...args)
    delete context[_fn]
    return result
}

// bind() 返回的函数可以作为构造函数,但是此时this失效
Function.prototype.mybind = function(context) {
    context = context || window
    let _this = this
    let agr1 = Array.prototype.call.slice(arguments,1) //第一次传入参数
    let f = function () {} //中转函数,防止修改绑定函数的prototype
    let F = function () {
       // 第二次传入参数
      let arg2 = Array.prototype.call.slice(arguments)
       // 以下等同于 _this.apply( _this instanceof context ? _this : context,arg1.contant(arg2))
       // _this instanceof context 判断bind返回函数是否作为构造函数
       context._fn = _this instanceof context ? _this : context
       let result = context._fn(arg1.contant(arg2))
       delete context._fn
       return result
    }
    // 继承绑定函数原型
    f.prototype = this.prototype
  	F.prototype = new f() // 因为实例都是独立的,所以修改F.prototype不影响,参考寄生组合继承
    return F
}

原型和原型链

概念

每个对象和函数都有隐式原型_prop_,只有函数才有显式原型prototype【Function.prototype除外】,_prop_形成原型链,prototype保存方法和变量,获取函数方法或变量时会先在本身寻找,找不到会顺着原型链往上找,直到找到 null 为止。

构造函数和实例的原型关系:instance.prop = instance.constructor.prototype

Object.creat(obj)

创建一个新对象,将新对象的_prop_连接到传入对象上。

// 实现原理:实例的_prop_指向构造函数的prototyp
function myCreat(proto, properties) {
  function F() {} // 作为构造函数
  F.prototype = proto
  if(properties) {
    Object.defineProperties(F,properties) // 设置函数属性
  }
  return new F()
}

new和构造函数

new操作符: 调用构造函数,为实例和构造函数(普通函数用于创建一类对象时)建立了一条原型链.

function myNew(CO,args) {
  let obj = Object.creat(CO.prototype) // let obj = {}; obj._prop_ = CO.prototype
  let result = CO.apply(obj,args)
  return Object.prototype.call.toString(result) === '[Object Object]' ? result : obj
}

继承

原型链继承

通过proptotype和构造函数实现。

缺点:

  1. 父类属性会被所有实例共享,导致创建实例可以篡改父类原型。
  2. 创建 Child 时,不能向 Parent 传参。
function Father() {
    const a = 10
}
function Child() {}
Child.prototytpe = new father()
child1 = new Child()
child1.a // 10
构造函数继承【经典继承】

通过call()和原型链继承实现。

优点:可以向父类传参。

缺点:

  1. 每次创建都会调用父类方法,耗费性能。
  2. 只能继承构造函数中的通过this绑定的属性和方法,无法继承构造函数原型上的方法和属性。
function Foo(name) {
    this.name = name;
    this.getName = function() {
        console.log("name:"+name)
    }
}
Foo.prototype.age = function() {
    console.log('父类:' + this.name);
};
Foo.prototype.obj = function() {
    console.log('hello world');
};
function fn (name) {
    Foo.call(this,name);
}
var a = new fn('lisi');
a.getName() // name:lisi
a.age() //Uncaught TypeError: a.age is not a function
组合继承

原型链+构造函数继承的结合。 优点:

  1. 可以传参。
  2. 不会篡改原型。
  3. 调用两次父构造函数【第一次:设置子类实例原型 Child.prototype = new Parent(); 第二次:创建子类类型 child = new Child()】 缺点:
  4. 创建的实例和原型上有两个相同的属性和方法。
function Parent (name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child (name, age) {
    Parent.call(this, name);   //第二次实际调用
    this.age = age;
}

Child.prototype = new Parent();   // 第一次调用
Child.prototype.constructor = Child;

var child1 = new Child('kevin', '18');  // 第二次调用
// 原型中会存在两份相同的属性/方法
原型式继承

通过中转函数将原型链接到父类上,原理同Object.creat。 缺点:

  1. 父类属性会被所有实例共享,导致创建实例可以篡改父类原型。
  2. 创建 Child 时,不能向 Parent 传参。
function father() {
    const a = 10
}

// 封装中转函数 Object.creat()原理
function contant(obj) {
  function F() {}
  F.prototype = obj
  return new F
}

let child = contant(father) 
寄生继承
function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

function extend(child, parent) {
    var prototype = object(parent.prototype); // prototype = Object.creat(parent.prototype)
    // prototype._prop_ = parent.prototype
    prototype.constructor = child;
    child.prototype = prototype;
}

// 当我们使用的时候:
extend(Child, Parent);
Class 继承
  1. 必须使用 new 调用,否则会报错
  2. class内部定义的方法都是不可枚举的,即使是后面添加的也一样
  3. class实际上也是函数
class A extends B {}
// 向父类传参 super,用constructor定义实例属性

class A extends null {}
A._prop_ === Function.prototype // true
A.prototype._prop_ === undefined // true
静态属性static

指class本身的属性,不会定义到实例上,只能通过类调用。子类可继承。

super()
  1. 代表父类的构造函数
  2. 只能用在子类的构造函数中
constructor 另一种写法

写在函数顶层,省略 constructor 和 this

ES5和ES6继承区别
  1. 类声明不会提升,函数声明会提升。
  2. ES6用super()拿到子类,ES5通过apply绑定。
  3. ES6实质上是先创建父类的实例对象this,然后再用子类的构造函数修改this。因为子类没有自己的this对象,所以必须先调用父类的super()方法,否则新建实例报错。
  4. ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.call(this))。
  5. ES5的内部方法是可枚举的,ES6不可枚举。

闭包

作用

设计私有变量和方法,避免全局污染,让变量始终在内存中。

特点

  1. 函数内嵌套函数。
  2. 内部函数可以引用外部参数和变量。
  3. 参数和变量不会被垃圾回收机制回收。
柯里化

闭包的一种应用形式。函数里包含函数,外部函数处理部分参数,里面函数处理其余参数。

作用:

  1. 参数复用
  2. 延迟返回
  3. 延迟计算/运行(bind())
// 柯里化经典题目
// 计算sum(1)(2)(3)...

垃圾回收机制(浏览器的js引擎机制)

方法

  1. 引用计数法【循环引用会造成内存泄漏】
  2. 标记清除法 栈内存的回收:

栈内存调用栈上下文切换后就被回收,比较简单。

堆内存:

分为新生代和老生代,新生代保存时间短的对象,老生代保存时间长的对象。

新生代主要采用scavenge算法,该算法将新手空间分为两部分,一部分为from,一部分为to。

首先检测from部分,检测到有存活的移动到to,然后释放from,再将to的存活对象移动到from。

多次复制后仍存活则晋升为老生代。(to空间超过限制也会晋升)

老生代采用Mark-sweep 和Mark-compact相结合的算法。(因为老生代的活动对象多,采用scavenge算法浪费空间且效率低)

Mark-sweep:标记清除。标记时采用增量标记,与js逻辑交替进行。

Mark-compact:标记移动。由于Mark-sweep清除后会产生部分空缺,需要将活动对象往一边堆叠。

清除方法参考:1.标记清除【V8】(进出环境进行标记);2.引用计数(创建对象时会产生引用计数对象,被引用时加1,删掉被引用变量减1,为0时被回收)

垃圾回收时机:利用浏览器渲染页面的空闲时间进行垃圾回收。

内存泄漏的情况

  1. 未使用的var变量
  2. console.log
  3. 闭包
  4. setTimeout 第一个参数为字符串【参数为字符串会调用eval进行解析,在eval的作用域中解析成js代码运行,由于进行了引用,所以不会被垃圾回收机制回收】
  5. 循环引用

Promise

原理:回调函数

状态:pending,fulfilled,rejected

一旦确定不可更改

方法

  1. promise.then()
  2. promise.catch()
  3. promise.finally()
  4. promise.all([promiseArr]) 一个失败就全为失败,返回第一个失败的结果
  5. prommise.any([promiseArr]) 一个成功就返回成功,返回的是第一个成功的值
  6. promise.race([promiseArr]) 只要有一个完成就完成,无论成功还是失败

Async await: Generator 函数语法糖,将异步转为同步形式

Promise 缺点

  1. 处于pedding时不能确定是刚开始还是快结束;
  2. 一旦开始不能停止;
  3. 不设置回调函数,Promise内部错误不反映到外部;

Promis手写实现【太长了,掘金上可以搜到很多】

Promis手写方法实现

// promise.catch()
promise.prototype.catch(callback=>{
  return this.then(null,callback)
})

//promise.finally
promise.prototype.finally(callback => {
  return this.then(data => {
    return resolve(callback).then(() => data)
  },err => {
    return reject(callback).then(() => throw err)
  })
})

// promise.all()
Promise.myAll(promises) {
  let arr = [] // 接收promises的返回值
  count = 0  // 设置计数器判断promises是否执行完
  return new Promise((resolve,reject) => {
    promises.foreach((item,i) => {
      Promise.resolev(item).then(res => {
        arr[i] = res
        count ++ 
        if(peomises.length === count) {resolev(arr)}
      }).catch(reject)
    })
  })
}

//promise.race()
Promise.race(promises) {
  return new Promise((resolve,reject) => {
    pormises.foreach(item => {
      Promise.resolve(item).then(resolve,reject)
    })
  })
}

// promise.any()
Promise.myAny(promises) {
  let arr = [] // 接收promises的返回值
  count = 0  // 设置计数器判断promises是否执行完
  return new Promise((resolve,reject) => {
    promises.foreach((item,i) => {
      Promise.resolev(item).then(res => {
        resolve
      }).catch((e)=>{
        arr[i] = res
        count ++ 
        if(peomises.length === count) {reject(arr)})
    })
  })
}

// promise.allsettled
Promise.myAllsettled(promises) {
    let arr = []
    return new Promise((resolve,reject) => {
      promises.foreach((item,i)=>{
        Promise.resolve(item).then((res=>{
            arr[i] = res
            count++
            if(count === promises.length) return arr
        	})
        ).catch(e=>{
          arr[i] = e
          count++
          if(count === promises.length) return arr
        })
      })
    })
}

async和await

和promise的联系:async 是 Generator 和 Promise 的结合

作用:用同步方式执行异步

本质:Generator函数语法糖【Generator 返回 Iterator】

返回值:Promise函数

实现原理:Generator函数和自动执行器一起封装

错误捕获:try..catch

// 多个命令
async function main() {
  try {
    const val1 = await firstStep();
    const val2 = await secondStep(val1);
    const val3 = await thirdStep(val1, val2);

    console.log('Final: ', val3);
  }
  catch (err) {
    console.error(err);
  }
}

// 利用 try..catch 进行多次尝试
const superagent = require('superagent');
const NUM_RETRIES = 3;

async function test() {
  let i;
  for (i = 0; i < NUM_RETRIES; ++i) {
    try {
      await superagent.get('http://google.com/this-throws-an-error');
      break;
    } catch(err) {}
  }
  console.log(i); // 3
}
test();