JavaScript核心知识总结(中)一

简介: JavaScript核心知识总结(中)一

前言

在上篇中,我们总结了 JavaScript 这门语言的基础知识,而这篇则是讲述这门语言的特色,也是它的核心知识、面试重点。

函数

函数可谓是JavaScript中的一等公民,与函数涉及的也有相当多的概念,当初笔者刚学的时候被绕的云里雾里,下面先以一个问题,开始函数的总结与学习。

怎么执行一段JavaScript代码

JavaScript 代码的执行主要分为以下三步:

  • 分析有没有词法、语法错误
  • 预编译-发生在函数执行的前一刻,生成变量环境、词法环境
  • 解释执行

JavaScript 比较有特色的就是在预编译阶段,我们重点看看预编译阶段做了什么事情。

预编译

引擎一开始会创建执行上下文(也叫Activation ObjectAO对象),执行上下文主要有如下三种类型:

  • 全局执行上下文:只有一个
  • 函数执行上下文:存在无数个,每个函数被调用就新建一个
  • Eval执行上下文:eval中运行的函数代码,很少用

执行上下文的创建主要分为创建阶段和执行阶段

创建阶段

1.绑定 this 指向

2.创建词法环境

3.生成变量环境

这里解释一下词法环境和变量环境,其实他们两个是差不多相同的组件。词法环境中包含两个部分,一个是存储变量与函数声明的位置,另一个是对外部环境的引用

伪代码如下:

GlobalExectionContext = {  // 全局执行上下文
  LexicalEnvironment: {       // 词法环境
    EnvironmentRecord: {      // 环境记录
      Type: "Object",            // 全局环境
      // 标识符绑定在这里 
      outer: <null>, // 对外部环境的引用
    }           
  }  
}

FunctionExectionContext = { // 函数执行上下文
  LexicalEnvironment: {     // 词法环境
    EnvironmentRecord: {      // 环境记录
      Type: "Declarative",       // 函数环境
      // 标识符绑定在这里         // 对外部环境的引用
      outer: <Global or outer function environment reference>  
  }  
}

变量环境也是一个词法环境,词法环境和变量环境的区别在于:

  • 词法环境存储函数声明和绑定 letconst 变量
  • 变量环境仅绑定 var 变量

执行阶段

完成对所有变量的分配,最后执行代码

作用域&作用域链

每个 JavaScript 函数都是一个对象,对象中有的属性可以访问,有的不能,这些属性仅供 JavaScript 引擎存取,如 [[scope]]

[[scope]]就是函数的作用域,其中存储了执行上下文的集合

[[scope]]中所存储的执行上下文对象的集合,这个集合呈链式链接,我们称这种链式链接为作用域链。查找变量时,要从作用域链的顶部开始查找。在当前执行上下文中找不到变量时,则到对外部环境的引用中向上查找,故呈现一个链式结构。

作用域与变量声明提升

  • JavaScript 中,函数声明与变量声明会被 JavaScript 引擎隐式地提升到当前作用域的顶部
  • 声明语句中的赋值部分并不会被提升,只有名称被提升
  • 函数声明的优先级高于变量,如果变量名跟函数名相同且未赋值,则函数声明会覆盖变量声明
  • 如果函数有多个同名参数,那么最后一个参数(即使没有定义)会覆盖前面的同名参数

闭包

当内部函数被保存到外部时,将会生成闭包。生成闭包后,内部函数依旧可以访问其所在的外部函数的变量。

当函数执行时,会创建执行上下文,获取作用域链(存储了函数能够访问的所有执行上下文)。函数每次执行时对应的执行上下文都是独一无二的,当函数执行完毕,函数都会失去对这个作用域链的引用, JS 的垃圾回收机制是采用引用计数策略,如果一块内存不再被引用了那么这块内存就会被释放。

但是,当闭包存在时,即内部函数保留了对外部变量的引用时,这个作用域链就不会被销毁,此时内部函数依旧可以访问其所在的外部函数的变量,这就是闭包。

即闭包逃过了 GC 策略,故滥用会导致内存泄漏,其实本身就是一种内存泄漏?

经典题目

for (var i = 0; i < 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 100)
}
function test() {
   var a = [];
   for (var i = 0; i < 5; i++) {
         a[i] = function () {
            console.log(i);
         }
   }
   return a;
}

var myArr = test();
for(var j=0;j<5;j++)
{
   myArr[j]();
}

以上两个例子都打印5个5,简单解释就是变量 i 记录的是最终跳出循环的值,即5,可以通过立即执行函数或者 let 来解决。因为立即执行函数创建了一个新的执行上下文,可以保存当前循环 i 的值,而let则构建了块级作用域,也可以保存当前循环 i 的值。

for (var i = 0; i < 5; i++) {
   ;(function(i) {
      setTimeout(function timer() {
         console.log(i)
      }, i * 100)
   })(i)
}
function test(){
   var arr=[];
   for(i=0;i<10;i++)
   {
      (function(j){
         arr[j]=function(){
         console.log(j);
         }
      })(i)
   }
   return arr;
}

var myArr=test();
for(j=0;j<10;j++)
{
   myArr[j]();
}

封装私有变量

function Counter() {
   let count = 0;
   this.plus = function () {
      return ++count;
   }
   this.minus = function () {
      return --count;
   }
   this.getCount = function () {
      return count;
   }
}

const counter = new Counter();
counter.puls();
counter.puls();
console.log(counter.getCount())

计数器

实现一个foo函数 可以这么使用:

a = foo();
b = foo();
c = foo();
// a === 1;b === 2;c === 3;
foo.clear();d = foo(); //d === 1;
function myIndex() {
    var index = 1;

    function foo(){
        return index++;
    }

    foo.clear = function() {
        index = 1;
    }
    return foo;
}

var foo = myIndex();

JavaScript 中,调用函数的方式?

JavaScript 中,调用函数的方式主要有如下数种

  • 方法调用模式 Foo.foo(arg1, arg2) ;
  • 函数调用模式 foo(arg1, arg2);
  • 构造器调用模式 (new Foo())(arg1, arg2);
  • call / apply 调用模式 Foo.foo.call(that, arg1, arg2);
  • bind 调用模式 Foo.foo.bind(that)(arg1, arg2)();

防抖节流

无论是面试还是业务开发,这都是经常接触到的知识,我们一起来看看

防抖 debounce

函数防抖就是在函数需要频繁触发的情况下,只有足够的空闲时间,才执行一次。

典型应用

  • 百度搜索框在输入稍有停顿时才更新推荐热词。
  • 拖拽
function debounce(handler, delay = 300){

  var timer = null;

  return function(){

    var _self = this,
        _args = arguments;

    clearTimeout(timer);
    timer = setTimeout(function(){
      handler.apply(_self, _args);
    }, delay);
  }
// 频繁触发时,清除对应的定时器,然后再开一个定时器,delay秒后执行
function debounce(handler, delay){

  delay = delay || 300;
  var timer = null;

  return function(){

    var _self = this,
        _args = arguments;

    clearTimeout(timer);
    timer = setTimeout(function(){
      handler.apply(_self, _args);
    }, delay);
  }
}

// 不希望被频繁调用的函数
function add(counterName) {
  console.log(counterName + ":  " + this.index ++);
}

// 需要的上下文对象
let counter = {
  index: 0
}

// 防抖的自增函数,绑定上下文对象counter
let db_add = debounce(add, 10).bind(counter)

// 每隔500ms频繁调用3次自增函数,但因为防抖的存在,这3次内只调用一次
setInterval(function() {
  db_add("someCounter1");
  db_add("someCounter2");
  db_add("someCounter3");
}, 500)


/**
 * 预期效果:
 * 
 * 每隔500ms,输出一个自增的数
 * 即打印:
    someCounter3:  0
    someCounter3:  1
    someCounter3:  2
    someCounter3:  3
 */

节流 throttle

一个函数只有在大于执行周期时才执行,周期内调用不执行。好像水滴积攒到一定程度才会触发一次下落一样。

典型应用:

  • 抢券时疯狂点击,既要限制次数,又要保证先点先发出请求
  • 窗口调整
  • 页面滚动
function throttle(fn,wait=300){
    var lastTime = 0
    return function(){
        var that = this,args=arguments
        var nowTime = new Date().getTime()
        if((nowTime-lastTime)>wait){
            fn.apply(that,args)
            lastTime = nowTime
        }
    }
}

this

在上面说函数的时候,我们也提到了一下 this ,即函数创建执行上下文的时候第一步就是绑定 this 的指向,也对应了那句话-- JavaScriptthis 的指向是当函数执行的时候才确定的。

this 的指向主要有如下数种:

  1. 作为函数直接调用,非严格模式下,this指向window,严格模式下,this指向undefined
  2. 作为某对象的方法调用,this通常指向调用的对象
  3. 使用applycallbind可以绑定this指向
  4. 在构造函数中,this指向新创建的对象
  5. 箭头函数没有单独的this值,this在箭头创建时绑定,它与声明所在的上下文相同

当多个this出现时,this改指向哪里?

首先, new 的方式优先级最高,接下来是 bind 这些函数,然后是 obj.foo() 这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。

  1. new 绑定
  2. 显式绑定-> bindapplycall
  3. 隐式绑定-> obj.foo()
  4. 默认绑定-> 浏览器环境默认是window

默认绑定

function foo() { // 运行在严格模式下,this会绑定到undefined
    "use strict";
    console.log( this.a );
}

var a = 2;

// 调用
foo(); // TypeError: Cannot read property 'a' of undefined

// --------------------------------------

function foo() { // 运行
    console.log( this.a );
}

var a = 2;
foo()//2

隐式绑定

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2,
    foo: foo
};

obj.foo(); // 2

注意下面这种情况,称为隐式丢失。

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2,
    foo: foo
};

var bar = obj.foo; // 函数别名

var a = "global"; // a是全局对象的属性

bar(); // "global"

显示绑定

function foo() {
    console.log( this.a );
}

var obj = {
    a: 2
};

foo.call( obj ); // 2  调用foo时强制把foo的this绑定到obj上

new 绑定

function foo(a) {
    this.a = a;
}

var bar = new foo(2); // bar和foo(..)调用中的this进行绑定
console.log( bar.a ); // 2

某厂面试

请分别写出下面题目的答案。

function Foo() {
    getName = function() {
        console.log(1);
    };
    return this;
}
Foo.getName = function() {
    console.log(2);
};
Foo.prototype.getName = function() {
    console.log(3);
};
var getName = function() {
    console.log(4);
};

function getName() {
    console.log(5);
}

//请写出以下输出结果:
Foo.getName();      //-> 2    Foo对象上的getName() ,这里不会是3,因为只有Foo的实例对象才会是3,Foo上面是没有3的
getName();          //-> 4    window上的getName,console.log(5)的那个函数提升后,在console.log(4)的那里被重新赋值
Foo().getName();    //-> 1    在Foo函数中,getName是全局的getName,覆盖后输出 1
getName();          //-> 1    window中getName();
new Foo.getName();  //-> 2    Foo后面不带括号而直接 '.',那么点的优先级会比new的高,所以把 Foo.getName 作为构造函数
new Foo().getName();//-> 3    此时是Foo的实例,原型上会有输出3这个方法

箭头函数中的this判断

箭头函数里面的 this 是继承它作用域父级的 this , 即声明箭头函数处的 this

let a = {
  b: function() {
    console.log(this)
  },
  c: () => {
    console.log(this)
  }
}

a.b()   // a
a.c()   // window

let d = a.b
d()     // window

bind、apply实现

自封装 bind 方法

  • 因为 bind 的使用方法是 某函数.bind(某对象,...剩余参数)
  • 所以需要在Function.prototype 上进行编程
  • 将传递的参数中的某对象和剩余参数使用 apply 的方式在一个回调函数中执行即可
  • 要在第一层获取到被绑定函数的 this ,因为要拿到那个函数用 apply
/**
 * 简单版本
 */
Function.prototype.myBind = (that, ...args) => {
  const funcThis = this;
  return function(..._args) {
    return funcThis.apply(that, args.concat(_args));
  }
}

Function.prototype.mybind = function(ctx) {
    var _this = this;
    var args = Array.prototype.slice.call(arguments, 1);
    return function() {
        return _this.apply(ctx, args.concat(args, Array.prototype.slice.call(arguments)))
    }
}
/**
 * 自封装bind方法
 * @param  {对象} target [被绑定的this对象, 之后的arguments就是被绑定传入参数]
 * @return {[function]}  [返回一个新函数,这个函数就是被绑定了this的新函数]
 */
Function.prototype.myBind = function (target){
  target = target || window;
  var self = this;
  var args = [].slice.call(arguments, 1);
  var temp = function(){};
  var F = function() {
    var _args = [].slice.call(arguments, 0);
    return self.apply(this instanceof temp ? this: target, args.concat(_args));
  }
  temp.prototype = this.prototype;    //当函数是构造函数时,维护原型关系
  F.prototype = new temp();
  return F;
}

自封装一个apply

  • 首先要先原型上即 Function.prototype 上编程
  • 需要拿到函数的引用, 在这里是 this
  • 让 传入对象.fn = this
  • 执行 传入对象.fn(传入参数)
  • 返回执行结果
Function.prototype.myApply = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  context = context || window
  context.fn = this
  let result
  // 处理参数和 call 有区别
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }
  delete context.fn
  return result
}

new实现

new的过程

  • 新生成一个对象
  • 链接到原型
  • 绑定this
  • 返回新对象
function create() {
    let obj = {}
    obj.__proto__ = con.prototype
    con.call(this)
    return obj
};

JavaScript核心知识总结(中)二

https://developer.aliyun.com/article/1495054

相关文章
|
10月前
|
JavaScript 前端开发
JavaScript基础总结
JavaScript基础总结
33 0
|
3月前
|
缓存 JavaScript 前端开发
JavaScript核心知识总结(下)一
JavaScript核心知识总结(下)一
|
2月前
|
JavaScript 前端开发
JavaScript基础
1 JavaScript书写位置 2 元素定位和样式设置 3 变量 4 事件 5 innerHTML属性 6 自定义函数
26 3
|
3月前
|
Web App开发 存储 JavaScript
JavaScript核心知识总结(上)
JavaScript核心知识总结(上)
|
3月前
|
存储 前端开发 JavaScript
JavaScript核心知识总结(中)二
JavaScript核心知识总结(中)二
|
3月前
|
JavaScript 前端开发 应用服务中间件
JavaScript核心知识总结(下)二
JavaScript核心知识总结(下)二
|
存储 JavaScript 前端开发
JavaScript基础(1)
JavaScript 是 Web 的编程语言。 所有现代的 HTML 页面都可以使用 JavaScript。 本教程将教你学习从初级到高级 JavaScript 知识。
118 1
|
JavaScript 前端开发
3 JavaScript基础使用
读前思考! 在使用之前要先明白2个问题: 在哪里写(用)JavaScript代码? 如何写JavaScript代码?
82 0
|
存储 移动开发 JavaScript
JavaScript基础(下)
JavaScript基础(下)
147 0
JavaScript基础(下)
|
JavaScript 前端开发
155个JavaScript基础问题(96-105)
155个JavaScript基础问题(96-105)
178 0
155个JavaScript基础问题(96-105)