Skip to main content

JS 进阶

ES6#

  • let const
  • 模板字符串 ``
  • 解构赋值
  • 扩展运算符 ...
  • 函数默认参数
  • 箭头函数
  • class
  • Map Set
  • Array.from() Array.of() find() fill()
  • repeat trim padStart
  • Promise
  • Module
  • symbol

ES7

  • includes
  • Math.pow()

作用域和闭包#

作用域#

js 采用词法作用域 lexical scoping,即静态作用域

静态作用域:函数的作用域在函数定义的时候就决定了

动态作用域:函数的作用域是在函数调用的时候才决定的

var value = 1;
function foo() {    console.log(value);}
function bar() {    var value = 2;    foo();}
bar();
// 静态作用域 -> 1// 动态作用域 -> 2
  • 全局作用域
  • 函数作用域
  • 块级作用域(ES6 新增)
if (true) {  let x = 100;}console.log(x); // ReferenceError
// 块级作用域 var x 变量提升了if (true) {  var x = 100;}console.log(x); // 100

自由变量#

所有的自由变量的查找,是在函数定义的地方,向上级作用域查找 而不是在执行的地方!!!

  • 一个变量在当前作用域没有定义,但被使用了
  • 向上级作用域,一层一层依次寻找,直至找到为止
  • 如果到全局作用域都没找到,则报错 xx is not defined

变量对象#

未进入执行阶段之前,变量对象VO中的属性都不能访问!但是进入执行阶段之后,变量对象VO转变为了活动对象AO,里面的属性都能被访问了,然后开始进行执行阶段的操作。其实都是同一个对象,只是处于执行上下文的不同生命周期

作用域链#

预编译流程#

暗示全局变量 imply global variable

a = 1;var a = 1;window.a = 1;

变量、函数声明提升#

  • 函数声明整体提升,变量只有声明提升,赋值不提升
  • 函数声明执行优先级优于变量提升
  • 同名的函数会覆盖同名函数与变量,但是同名的变量不会覆盖函数
  • 在上下文的执行阶段,同名函数会被变量重新赋值
  • AO activation object 活跃对象 函数上下文
  • GO global object 全局上下文
function test(a) {  console.log(a);  var a = 1;  console.log(a);  function a() {}  console.log(a);  var b = function () {};  console.log(b);  function d() {}}test(2); // ƒ a(){} 1 1 ƒ (){}
/* * AO activation object 活跃对象 函数上下文 * 1、寻找形参和变量声明 * 2、实参值赋值给形参 * 3、找函数声明 赋值 * 4、执行 * AO = {        a:undefined-->2-->function a(){}-->1        b:undefined-->function(){}          d:function d(){}    } */
console.log(a); //function a(){...}var a = 1;function a() {    console.log(2);}
/* * GO global object 全局上下文 * 1、寻找变量 * 2、找函数声明 * 3、执行 * GO = {        a:undefined-->function a(){}-->1    } * GO === window */
function test() {  var a = (b = 1);  console.log(a);}test();console.log(a, b); //err 1 // b被挂载到全局变量window上了
/* * GO = {        b:1    } * AO = {        a:undefined-->1    } */
var b = 3;console.log(a);function a(a) {  console.log(a);  var a = 2;  console.log(a);  function a() {}  var b = 5;  console.log(b);}a(1); //function a(a){...}  function a(){}  2   5
/* * GO global object 全局上下文 * 1、寻找变量 * 2、找函数声明 * 3、执行 * GO = {        b:undefined-->3        a:function a(a){...}    } * AO = {        a:undefined-->1-->function a(){}-->2        b:undefined-->5    } * AO里面有值就不会去GO里面查找即使是undefined */

函数的作用域链上 永远都会挂载GO 当函数执行时(前一刻,预编译时) 函数的AO便会挂载在作用域链的首位

function a() {  function b() {    function c() {}    c();  }  b();}a(); 
/* * a定义 a.[[scope]] --> 0:GO * a执行 a.[[scope]] --> 0:a-->AO 1:GO *  * b定义 b.[[scope]] --> 0:a-->AO 1:GO * b执行 b.[[scope]] --> 0:b-->AO 1:a-->AO 2:GO *  * c定义 c.[[scope]] --> 0:b-->AO 1:a-->AO    2:GO * c执行 c.[[scope]] --> 0:c-->AO 1:b-->AO    2:a-->AO    3:GO *  * c结束 c.[[scope]] --> 0:b-->AO 1:a-->AO    2:GO * * b结束 b.[[scope]] --> 0:a-->AO 1:GO    c.[[scope]]销毁 *  * a结束 a.[[scope]] --> 0:GO b.[[scope]]销毁 */

闭包#

danger

所有的自由变量的查找,是在函数定义的地方,向上级作用域查找 而不是在执行的地方!!!

关于闭包#

  • 闭包是指那些能够访问自由变量的函数
  • 定义在一个函数内部的函数
  • 闭包就是在函数里面声明函数并返回,当一个函数能够访问和操作另一个函数作用域中的变量时(即作用域链),就构成了一个闭包
  • 闭包就是能够读取其他函数内部变量的函数。在JS中,只有函数内部的子函数才能读取局部变量。

作用:可以读取函数内部的变量,让这些变量的值始终保持在内存中,不会被垃圾回收机制回收

优点:使用闭包可以隐藏变量以及防止变量被篡改和作用域的污染,从而实现封装

缺点:缺点就是由于保留了作用域链,会增加内存的开销。因此需要注意内存的使用,并且防止内存泄露的问题。(所以在退出函数之前,将不使用的局部变量全部删除。

闭包案例

  • 模块化
  • 防抖和节流
  • 函数柯里化
  • 匿名自执行函数
  • 缓存计算比较复杂的函数的结果
  • 封装
// 当内部函数被返回到外部并保存时,一定会产生闭包。// 闭包会产生原来的作用域链不释放,过度的闭包可能会导致内存泄漏,或加载过慢。function outer() {    var count = 0; //这个变量外部不可直接使用(可理解为受保护变量)    return function () {        count++; //通过内部函数操作受保护变量        console.log(count);    };}var inner = outer(); //调用外部函数获取内部函数inner(); //调用内部函数操作受保护变量
  • 函数作为返回值
function create() {    const a = 100;    return function () {        console.log(a); // a是自由变量    };}const fn = create();const a = 200;
console.log(fn()); //100
  • 函数作为参数被传递
function print(fn) {    const a = 200;  fn();}const a = 100;function fn() {    console.log(a); // a是自由变量}console.log(print(fn)); //100

隐藏数据#

// 闭包隐藏数据,只提供APIfunction createCache() {  const data = {} // 闭包中的数据,被隐藏,不被外界访问  return {    set: function (key, val) {      data[key] = val    },    get: function (key) {      return data[key]    },  }}const c = createCache()c.set('a', 100)console.log(c.get('a')) //100

实现私有变量#

var obj = {  a: 1,  get() {    return this.a;  },  set(val) {    this.a = val % 2 === 1 ? val : this.a;  },};

构造函数

function Closure(init) {  var p_val = init;  this.get = function () {    return p_val;  };  this.set = function (val) {    p_val = val;  };}const closure = new Closure(1);

defineProperty

let obj = {};let aValue = 1;Object.defineProperty(obj, "a", {  // value: 1,   // 如果一个描述符同时拥有 value 或 writable 和 get 或 set 键,则会产生一个异常。  get: function () {    return aValue;  },  set: function (val) {    aValue = val % 2 === 1 ? val : aValue;  },});

class

class Obj {  constructor() {    this._a = 1;  }  set a(val) {    if (val % 2 === 1) this._a = val;    else this._a = this._a;  }  get a() {    return this._a;  }}

关于 This#

  • thisJS的一个关键字 是当前环境执行期上下文对象的一个属性
  • this 的取值是在函数执行的时候决定的而不是在函数定义的
  • 全局 => window
  • 类 构造函数 => 实例对象
  • call apply bind => 传入什么绑定什么
  • 作为对象方法被调用 => this 指向上级对象
  • class 方法中调用 => 返回当前实例本身
  • 箭头函数 => 找它上级作用域 this 的值来决定
  • 箭头函数中的this是在定义函数的时候绑定,而不是在执行函数的时候绑定。继承的是父执行上下文里面的this

全局作用域#

  • 全局作用域下的this -> 全局对象
  • windowthis 的关系:this === window // true

This 在不同环境下的全局指向不同

this -> 浏览器 node web worker
获取全局对象web: window self frames thisnode: globalweb worker: self
通用 globalThis

对象#

  • this 总是指向调用这个函数的对象
const obj = { a: 1 };
Object.defineProperty(obj, 'a', {  get: function () {    console.log(this); // obj  },});

构造函数#

  • this 指向新创建的对象
function Test() {  this.a = 1;  console.log(this);}
const test = new Test(); // Test {a: 1}

回调函数#

let arr = [1, 2, 3];arr.forEach(function () {  console.log(this); // window});
setTimeout(function () {  console.log(this); // this 指向 Window});

箭头函数#

  • 箭头函数中的 this 指向外层作用域的 this 指向
  • 箭头函数的 call、apply、bind 不会改变 this 指向
const obj = { a: 1 };
obj.test = function () {  // let _this = this  console.log('test', this);  var t1 = () => {    console.log('t1', this);    var t2 = () => {      // t1 是箭头函数 t1 t2 指向 this -> obj      // t1 是普通函数 t1 t2 指向 this -> window      console.log('t2', this);    };    t2();  };  t1();};obj.test();

事件处理函数#

  • 事件处理函数内部的this总是指向被绑定的 DOM 元素
<button id="J_Btn">Test</button><script>  ;(function (doc) {    var oBtn = doc.getElementById('J_Btn')    function Add(a, b) {      this.a = a      this.b = b
      this.init()    }    Add.prototype.init = function () {      this.bindEvent()    }    Add.prototype.bindEvent = function () {      // oBtn.addEventListener('click', this.handleBtnClick, false)// NAN      // oBtn.addEventListener('click', this.handleBtnClick(), false) // 虽然输出3 但是加括号会立即执行 并不是我们预期的效果      console.log(this) //  Add {a: 1, b: 2}      oBtn.addEventListener('click', this.handleBtnClick.bind(this), false) // bingo 将函数内部this指向Add实例      // var _self = this      // oBtn.addEventListener('click', function () {      //   _self.handleBtnClick()      // }) // bingo    }    Add.prototype.handleBtnClick = function () {      console.log(this.a + this.b)    }    window.Add = Add  })(document)
new Add(1, 2)</script>

函数#

一个函数接收另一个函数做为参数,或者返回另一个函数,都是高阶函数

function func(a, b) {}console.log(func.name, func.length); // func 2

定义函数#

  • 函数声明:function fn(){}
  • 函数表达式:let fn = function(){}

区别:

  • 通过 var 定义函数表达式 会使得变量提升为 undefined
  • 函数声明会在代码执行前预加载,而函数表达式不会

参数#

  • 形参&实参
function test(a, b) {  console.log(test.length);  console.log(arguments.length);}test(1, 2, 3, 4); //2   4
//arguments 是一个js的关键字 代表传递进来的所有参数  是一个类数组
//函数内部可以更改实参的值
  • 默认参数
function greeting(name = 'honjay') {  console.log('hello ' + name);}greeting(); 
//对于多个参数function greetingWeather(name = 'honjay', weather) {    console.log('hello ' + name + 'today weather: ' + weather);}greetingWeather(undefined, 'sunny'); //使用undefined 只修改第二个参数
斐波那契数列#
  • 正常递归
function fn(n) {  if (n == 1 || n == 2) return 1;  return fn(n - 1) + fn(n - 2);}console.log(fn(5));
  • 尾递归优化
function Fibonacci2(n, ac1 = 1, ac2 = 1) {  if (n <= 1) {    return ac2;  }
  return Fibonacci2(n - 1, ac2, ac1 + ac2);}
阶乘#
  • 正常递归
function fn(n) {  if (n == 1) return 1;  return n * fn(n - 1);}console.log(fn(5));
  • 尾递归优化
function factorial(n, total) {  if (n === 1) return total;  return factorial(n - 1, n * total);}
factorial(5, 1); // 120

函数柯里化#

  • 柯里化:把接收多个参数的函数变换成接收一个单一参数的函数
  • 函数柯里化思想:预处理,降低通用性,提高适用性
  • 柯里化是一种函数式编程的技术
  • 只传递给函数一部分参数来调用它,并返回一个函数去处理剩下的参数

实现add(1,2) = add(1)(2)

function add(...a) {  return a.length > 1                 ? a.reduce((a, b) => a + b)                 : (b) => a[0] + b;}// a -> [1] b -> 2console.log(add(1, 2));console.log(add(1)(2));

实现add(1,2,3) = add(1)(2)(3)

利用柯里化的思想,将输入参数存储到数组中,等到数组长度和设定好一样再求值

const currying = (fn, len) => {  return function curried(...arr) {    if (arr.length >= len) {      return fn(...arr);    }    return (...arr2) => curried(...arr, ...arr2);  };};
const add = currying((...arr) => arr.reduce((a, b) => a + b, 0), 4);

valueOf

function add() {  var args = [...arguments];
  var fn = function () {    var arg_fn = [...arguments];    return add.apply(null, args.concat(arg_fn));  };
  fn.valueOf = function () {    return args.reduce((a, b) => a + b);  };
  return fn;}
console.log(add(1, 2, 3, 4));console.log(add(1)(2)(3)(4));console.log(add(1)(2, 3)(4));

函数式编程#

  • 独立于程序状态或全局变量,只依赖于传递给它们的参数进行计算
  • 限制更改程序状态,避免更改保存数据的全局对象
  • 对程序的副作用尽量小

立即执行函数#

IIFEImmediately Invoked Function Expression;(function (){})()
独立的作用域执行完成以后 自动销毁模拟模块化 向外抛出属性 和 方法

箭头函数区别#

  • 箭头函数没有原型 (prototype) 所以箭头函数本身没有 this,箭头函数里面的this指向继承自外层第一个普通函数的this
  • call、apply、bind 无法改变箭头函数中this的指向
  • 箭头函数不能作为构造函数使用,即不能通过new调用
  • 箭头函数没有自己的arguments对象 通过rest ...来获取参数

Function#

关于构造函数#

return this#
  • 相对于普通函数,构造函数中的this是指向实例的,而普通函数调用中的this是指向windows
  • 构造函数里默认隐式返回this
  • 如果手动返回this 或者 return 简单数据类型 会忽略 没有任何影响
  • 如果return Object 不再返回this对象 返回Object
function Dog(name, age) {  this.name = name;  this.age = age;  return { name: age };  // return 1; return this; 不写 都不影响}
const dog = new Dog('a', 1);console.log(dog); // { name: 1 }// Dog { name: 'a', age: 1 }
Function#
  • 每个 JavaScript 函数实际上都是一个 Function 对象
  • (function(){}).constructor === Function // true
// const test = new Function("a", "b", "c", "console.log(a+b+c)");const test = new Function('a, b, c', 'console.log(a+b+c)');test(1, 2, 3); // 上述两种发放都能返回 6
var t1 = new Function('console.log("t1")');var t2 = Function('console.log("t2")');t1(); // t1t2(); // t2
t1.__proto__ === Function.prototype; // trueFunction.__proto__ === Function.prototype; // true

Function 构造器与函数声明之间的不同

Function 构造器创建的函数不会创建当前环境的闭包,它们总是被创建于全局环境,因此在运行时它们只能访问全局变量和自己的局部变量,不能访问它们被 Function 构造器创建时所在的作用域的变量。

var x = 10;
function fn1() {  var x = 20;  return new Function('return x'); // 这里的 x 指向最上面全局作用域内的 x}
function fn2() {  var x = 20;  return function f() {    return x; // 这里的 x 指向上方本地作用域内的 x  };}
console.log(fn1()()); // 10console.log(fn2()()); // 20
构造函数题目#
function Foo() {  // 没用 var 声明,全局变量赋值  // 如果Foo没有执行 那么下面的赋值行为肯定是不会执行的  getName = function () {    console.log(1);  };  // console.log(this); // window  return this;}// 函数Foo上的静态方法 -> 一个函数对象上的静态方法/属性Foo.getName = function () {  console.log(2);};// 扩展函数原型上的方法// var foo = new Foo() ->foo.getName// new Foo.getNameFoo.prototype.getName = function () {  console.log(3);};// 给全局变量赋值为一个匿名函数var getName = function () {  console.log(4);};// 函数声明function getName() {  console.log(5);}/** * GO{ *    getName: *      undefined -> *      function getName () {} -> *      function(){console.log(4)} * } */Foo.getName(); // 2 执行Foo函数上的静态方法getName(); // 4 函数声明优先级高于函数表达式 但是函数表达式后面又覆盖掉函数声明Foo().getName(); // 1 Foo() 返回的 window.getName()getName(); // 1new Foo.getName(); // 2 访问new Foo().getName(); // 3new new Foo().getName(); // 3
demo#
class Father {  // constructor() {  //   // 目的:让函数内部的this指向固定  //   this.eat = this.eat.bind(this)  // }  get fruit() {    return 'apple';  }  eat() {    console.log('I am eating an ' + this.fruit);  }}
class Son {  get fruit() {    return 'orange';  }}
const father = new Father();const son = new Son();
father.eat();son.eat = father.eat;son.eat();
// 如果想让 son 也输出 I am eating an apple 呢// -> 在Father的构造器里面设置this.eat

静态方法 实例方法#

  • 实例方法
    • 通过对象原型定义 引用实例来调用
  • 静态方法
    • 直接定义 直接调用
function Fn() {  this.hello = function () {    console.log('实例方法');  };}
Fn.hello = function () {  console.log('静态方法');};
Fn.prototype.hello = function () {  console.log('实例共享方法(对象方法)');};
Fn.hello(); // 静态方法new Fn().hello(); // 实例方法 || 实例共享方法(对象方法)new Fn.hello(); // 静态方法
QA#
  • 为什么静态方法不能使用 this

    • 因为this代表的是调用这个函数的对象的引用,而静态方法是属于类的,不属于对象,静态方法成功加载后,对象还不一定存在
  • 为什么要有静态方法?

    • 有些东西是不需要实例的,只要有类就存在的,比如Array.isArray(obj);
      • eg: 判断一个对象是不是数组,如果这个方法是数组实例才有的,那就无法判断了。