[JS] 我所理解的 JavaScript (下)

2025 年 3 月 24 日 星期一(已编辑)
15
这篇文章上次修改于 2025 年 3 月 24 日 星期一,可能部分内容已经不适用,如有疑问可询问作者。

[JS] 我所理解的 JavaScript (下)

接触 Javascript 这门编程语言,满打满算应该有将近 4 年的时间,我觉得不能说对这门语言理解的有多深刻,只是在看了众多的经典书籍和博客之后,想把自己的理解和总结也分享出来。

This

  • 《你不知道的 JavaScript 下 》这本书当中关于 this 内容我觉得很好的阐述了 this 的绑定规则。
    • 默认绑定: 独立函数调用, 直接调用一个函数, 可以将这条规则看作是无法引用与其他规则的默认规则。(全局函数当中的 this 在非严格模式下等于 window, 在严格模式下等于 undefined)。
    • 显示绑定: 通过 apply 和call 来显示绑定this, 还有一种显示绑定的变种是硬绑定 bind()。
    • 隐式绑定:比如调用对象的方法那么方法当中的 this 会指向该对象。 隐式绑定有 this 丢失问题, 函数别名var bar = obj.foo; bar(); 传入函数参数, 传入语言内置的函数(setTimeout 函数,点击事件)都会导致 this 丢失。
    • new 绑定: .绑定构造函数当中的 this 到实例对象.
      • new 一个构造函数发生了什么: 1.创建一个空对象(实例对象)。 2.将实例对象的__proto__指定到构造函数的prototype上。 3.绑定构造函数当中的 this 到实例对象, 执行构造函数当中的代码,给实例对象赋值属性和方法。 4.构造函数当中是否有返回值 ,没有就返回这个实例对象, 有就看返回的是否是对象类型, 是就返回这个对象, 不是就返回实例对象。

this 的四项绑定规则案例

  • 默认绑定
// 默认绑定: 独立函数调用
// 1.案例一:
function foo() {
  console.log(this);
}
foo();

// 2.案例二:
function foo1() {
  console.log(this);
}

function foo2() {
  console.log(this);
  foo1();
}

function foo3() {
  console.log(this);
  foo2();
}

foo3();

// 3.案例三:
function foo() {
  function bar() {
    console.log(this);
  }
  return bar;
}

var fn = foo();
fn(); // window
  • 隐式绑定
// 案例一
var obj = {
  name: 'why',
  foo: function () {
    console.log(this);
  },
};

var bar = obj.foo; // 函数别名 this丢失
bar(); // window
// 案例二
function foo() {
  console.log(this);
}
var obj = {
  name: 'why',
  foo: foo,
};

var bar = obj.foo; // 函数别名 this丢失
bar(); // window
// 案例三
var a = 'global';
function foo(fn) {
  fn();
}
var obj = {
  a: 'local',
  eating() {
    console.log(this.a, '正在吃饭'); // 函数参数 this丢失
  },
};
foo(obj.eating); // global 正在吃饭
// 注意在node环境下调试的时候,全局的this指向空对象
// 案例四
setTimeout(() => {
  console.log(this); // window 传入系统内置的函数
}, 1000);
// 案例五
div.addEventListener('click', function () {
  console.log(this); // Dom div 传入系统内置的函数
});
// 案例6 map filter
arr.map((item) => {
  console.log(this); //默认是window第二个参数可以指定绑定 this 传入系统内置的函数
}, obj);
  • 显示绑定
function foo() {
  console.log('函数被调用了', this);
}
var obj = {
  name: 'obj',
};

// call / apply是可以指定this的绑定对象;
foo.call(obj, 参数);
foo.apply(obj, 参数列表);
foo.apply('aaaa');
  • new 绑定
function Person(name, age) {
  this.name = name;
  this.age = age;
}

var p1 = new Person('why', 18);
console.log(p1.name, p1.age);

this 绑定的优先级

new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定。

  • 显示绑定优先级高于隐式代码
const obj = {
  name: 'ryan',
  getName() {
    return this.name;
  },
};
console.log(obj.getName.call('string')); // this -> string;
//更明显的比较
function foo() {
  console.log(this);
}

var obj = {
  foo: foo.bind('aaa'),
};
obj.foo(); // this -> 'aaa';
  • new 绑定大于隐式绑定代码
var obj = {
  name: 'ryan',
  getName() {
    return this.name;
  },
};
let fn = new obj.getName(); this -> 实例对象fn
  • new 绑定大于显示绑定代码
// 由于new 和 call以及apply都是调用函数,自然就不可以一起使用。
function foo() {
  console.log(this);
}
const fn = foo.bind('a');
const nFn = new fn();
console.log(nFn); this -> 实例对象nFn

特殊绑定

  • 忽略显示绑定
// apply/call/bind: 当传入 null/undefined 时, 自动将 this 绑定成全局对象
foo.apply(null);
foo.apply(undefined);
  • 规范绑定
var obj1 = {
  name: 'obj1',
  foo: function () {
    console.log(this);
  },
};

var obj2 = {
  name: 'obj2',
};
(obj2.bar = obj1.foo)();

箭头函数当中的 this

  • 箭头函数不绑定 this,没有 arguments,也不可以作为构造函数 使用 new 来调用。
  • 箭头函数当中的 this 指向外层全局或者函数作用域当中的 this。
var obj = {
  data: [],
  getData: function () {
    setTimeout(() => {
      var result = ['abc', 'cba', 'nba'];
      this.data = result;
    }, 2000);
  },
};

this 面试题

面试题误区

var obj = {
  name: 'ryan',
  getName: () => {
    return this.name;
  },
};

// ""
  • 如果真的弄懂了以上规则,刷题如切菜般容易。
  • getName 的外层作用域是全局作用域,而不是 obj 对象大括号作用域。因为在 js 当中只有全局作用域和函数作用域。
  • 给箭头函数绑定 this 是徒劳的((() => {}).call('string'))。

  • 面试题 1
var name = 'window';

var person = {
  name: 'person',
  sayName: function () {
    console.log(this.name);
  },
};

function sayName() {
  var sss = person.sayName;
  sss(); // window: 独立函数调用
  person.sayName(); // person: 隐式调用
  `(person.sayName())`; // person: 隐式调用
  (b = person.sayName)(); // window: 赋值表达式(独立函数调用)

sayName();
  • 面试题 2
var name = 'window';

var person1 = {
  name: 'person1',
  foo1: function () {
    console.log(this.name);
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name);
    };
  },
  foo4: function () {
    return () => {
      console.log(this.name);
    };
  },
};

var person2 = { name: 'person2' };

person1.foo1(); // person1(隐式绑定)
person1.foo1.call(person2); // person2(显示绑定优先级大于隐式绑定)

person1.foo2(); // window(不绑定作用域,上层作用域是全局)
person1.foo2.call(person2); // window

person1.foo3()(); // window(独立函数调用)
person1.foo3.call(person2)(); // window(独立函数调用)
person1.foo3().call(person2); // person2(最终调用返回函数式, 使用的是显示绑定)

person1.foo4()(); // person1(箭头函数不绑定this, 上层作用域this是person1)
person1.foo4.call(person2)(); // person2(上层作用域被显示的绑定了一个person2)
person1.foo4().call(person2); // person1(上层找到person1)
  • 面试题三
var name = 'window';

function Person(name) {
  this.name = name;
  (this.foo1 = function () {
    console.log(this.name);
  }),
    (this.foo2 = () => console.log(this.name)),
    (this.foo3 = function () {
      return function () {
        console.log(this.name);
      };
    }),
    (this.foo4 = function () {
      return () => {
        console.log(this.name);
      };
    });
}

var person1 = new Person('person1');
var person2 = new Person('person2');

person1.foo1(); // person1
person1.foo1.call(person2); // person2(显示高于隐式绑定)

person1.foo2(); // person1 (上层作用域中的 this 是 person1)
person1.foo2.call(person2); // person1 (上层作用域中的 this 是 person1)

person1.foo3()(); // window(独立函数调用)
person1.foo3.call(person2)(); // window
person1.foo3().call(person2); // person2

person1.foo4()(); // person1
person1.foo4.call(person2)(); // person2
person1.foo4().call(person2); // person1

var obj = {
  name: 'obj',
  foo: function () {},
};
  • 面试题四
var name = 'window';

function Person(name) {
  this.name = name;
  this.obj = {
    name: 'obj',
    foo1: function () {
      return function () {
        console.log(this.name);
      };
    },
    foo2: function () {
      return () => {
        console.log(this.name);
      };
    },
  };
}

var person1 = new Person('person1');
var person2 = new Person('person2');

person1.obj.foo1()(); // window 独立函数调用
person1.obj.foo1.call(person2)(); // window
person1.obj.foo1().call(person2); // person2

person1.obj.foo2()(); // obj
person1.obj.foo2.call(person2)(); // person2
person1.obj.foo2().call(person2); // obj

异步

异步概述

异步是计算机科学的一个基本概念,其中一种含义是计算机多线程的异步处理 ,也可以说异步行为就是在等待其他操作完成的同时,也可以运行其他指令,这样可以优化计算量大,时间长的操作。Java/go 语言当中可以通过线程池 runtime /goroutine 来实现异步,但是对于我们 JavaScript 这种单线程语言(一次只能完成一个任务)。我们是通过 JavaScript 的事件循环模型,结合浏览器多线程管理回调函数作为异步解决方案。也就是将 DOM操作,HTTP 请求,定时器等的回调函数都交给浏览器的对应线程来管理,有了操作结果之后就将回调函数加入 JavaScript 事件循环模型的消息队列当中,当 JavaScript主线程空闲时,就会轮询消息队列,将异步任务读取到调用栈当中等待主线程的顺序执行。

异步解决方案

在早期 JavaScript 中,只支持回调函数来处理异步操作返回的结果。之后出现的 Promise 和 Async 其实本质上仍然是在处理和表达回调函数。只不过从我们作为开发者的角度来说,我们可以用一些优雅的,安全的,可以信任的语法来换一种形式来处理异步回调函数。

早期回调函数

早期回调函数处理异步回调的缺陷十分明显:

  • 第一个是线性理解能力的缺失,我们期望的异步方式是有计划的,线性的。比如:我通过 HTTP 请求,获取数据之后在操作数据,但是回调在表达异步的时候是非顺序的,非线性的。比如:我先定义好拿到数据之后的操作,再去 HTTP 请求。
  • 第二个是嵌套回调异步,如果异步返回值又依赖另一个异步返回值,回调的情况还会进一步复杂,最致命的是如果试图向其中添加新特性,他就立马会变得很难以拓展。
  • 第三个是信任缺失,因为没有一种统一的。我们交给第三方库的回调函数,控制权在第三方库手里。 所以我们需要一种更好的方案来优雅的处理异步回调函数。

Promise 处理异步回调

通过手写Promise源码过后,我发现Promise在处理异步回调的核心是每次resovle的时候,resovle会挂起一个异步回调任务,然后then同步传递的回调函数放到这个异步任务当中去,之后这个异步回调任务调用then函数。如果是链式调用then之后又继续resovle

之后从三个方面简单介绍一下 Promise。Promise 特性,Promise 静态方法,Promise 实例方法,Promise 缺陷,Promise解决了什么样的问题。

  • Promise 是一个有三种状态的对象,pending(待定), fufilled(兑现), rejected(拒绝)。无论落定为哪种状态都是不可逆的。Promise 的状态转换为兑现,就会有一个兑现的值 value,转换为拒绝,就会有拒绝的原因 reason。
  • Promise 静态方法 :Promise.resove(), Promise.reject(), Promise.all(), Promise.race(),Promise.allSettled(), Promise.any()。
  • Promise 实例方法 : Promise.prototype.then(), Promise.prototype.catch(), Promise.prototype.finally()。

Promise 嵌套

// bad
loadSomething().then(function(something) {
    loadAnotherthing().then(function(another) {
        DoSomethingOnThem(something, another);
    });
});

复制代码
// good
Promise.all([loadSomething(), loadAnotherthing()])
.then(function ([something, another]) {
    DoSomethingOnThem(...[something, another]);
});

2.断开的 Promise 链

// bad
function anAsyncCall() {
    var promise = doSomethingAsync();
    promise.then(function() {
        somethingComplicated();
    });

    return promise;
}

// good
function anAsyncCall() {
    var promise = doSomethingAsync();
    return promise.then(function() {
        somethingComplicated()
    });
}

3.混乱的集合

// bad
function workMyCollection(arr) {
    var resultArr = [];
    function _recursive(idx) {
        if (idx >= resultArr.length) return resultArr;

        return doSomethingAsync(arr[idx]).then(function(res) {
            resultArr.push(res);
            return _recursive(idx + 1);
        });
    }

    return _recursive(0);
}

你可以写成:

function workMyCollection(arr) {
    return Promise.all(arr.map(function(item) {
        return doSomethingAsync(item);
    }));
}

Promise 解决了什么样的问题

回调嵌套

request(url, function(err, res, body) {
    if (err) handleError(err);
    fs.writeFile('1.txt', body, function(err) {
        request(url2, function(err, res, body) {
            if (err) handleError(err)
        })
    })
});

使用 Promise 后:

request(url)
.then(function(result) {
    return writeFileAsynv('1.txt', result)
})
.then(function(result) {
    return request(url2)
})
.catch(function(e){
    handleError(e)
});

控制反转再反转

使用第三方回调API 的时候,可能会遇到如下问题:

  1. 回调函数执行多次 --这个Promise每次只可以resovle一次,剩下的调用会被忽略。

Promise 缺陷

  1. 无法中途取消
  2. 无法得知 pending 状态,不知道目前进展到哪一个方面
  3. 单一值(每次只能有一个完成值或者拒绝原因,我们只能构造对象和数组去解构)
  4. 吃掉错误,这块项目当中会很常见:一般会和async await 以及try catch相互配合

如果我们在catch块捕获到了错误,如果你只是做了简单的返回,或者打印,最上层的try catch块是捕获不到的,只有throw抛出错误或者return Promise.reject()才可以被外面的try catch同步捕获到来进行处理。而try catch块同样有这样的局限,也会在 catch 块中吃掉错误,如果想要被上层处理,还得继续抛出。我在项目当中的实践就是如果try catch太多,太冗余,我会使用catch返回错误,去用if判断,可以大大减少冗余。

async,await 处理异步回调

async函数就是Generator函数 + Promise的语法糖,为什么这么说呢?因为我们发现async函数的本质,就是将Generator函数和 co | thunk 执行器包装再了一个async函数里。

Generator 处理异步回调

function* getData(url) {
  const re1 = yield requestData(url);
  const re2 = yield requestData(res1 + url);
  const re3 = yield requestData(res2 + url);
}

手动执行

getData("user_1")
  .next("name")
  .value.then((res) => {
    getData(res)
      .next("id")
      .value.then((res) => {
        getData(res)
          .next()
          .value.then((res) => {
            console.log(res);
          });
      });
  });

co 模块自动执行器

function generator(generatorFun) {
  const generator = generatorFun();
  function execu(res) {
    const generatorObj = generator.next(res);
    if (!generatorObj.done) {
      generatorObj.value.then((res) => {
        execu(res);
      });
    } else {
      return generatorObj.value;
    }
  }
  execu(res);
}

我们用Generator实现异步回调处理的时候,通过生成器函数,yield请求函数a,请求函数b,请求函数c来表达异步处理,通过手动调用.next.then再继续嵌套调用.next.then来执行,之后有了co或者thunk执行器模块,可以帮我们去自动执行,回过头来再看async的写法,发现async函数的本质,就是将Generator函数和执行器包装再了一个async函数里。

async await 处理异步

// async await
async function getData(url) {
  const re1 = await requestData(url);
  const re2 = await requestData(res1 + url);
  const re3 = await requestData(res2 + url);
}

async 比 Promise的优势?

  • 代码更加简洁。--这个显而易见,我们不用每次把一大把逻辑都放在then处理函数当中。
  • 错误处理。 --由于try catch只能捕获同步错误,所以不能或者说难以捕获Promise 异步回调的错误,async/await的出现使得try/catch 就可以同步捕获异步错误。
  • 调试。 --- 对于await 我们可以按照顺序调试,而Promise并不会顺序执行。

async 陷阱

指开发者贪图语法上的简洁而让原本可以并行执行的内容变成了顺序执行,从而影响了性能

(async () => {
  const getList = await getList();
  const getAnotherList = await getAnotherList();
})();

getList()和getAnotherList() 其实并没有依赖关系,但是现在的这种写法,虽然简洁,却导致了 getAnotherList() 只能在 getList() 返回后才会执行,从而导致了多一倍的请求时间。

(async () => {
  const listPromise = getList();
  const anotherListPromise = getAnotherList();
  await listPromise;
  await anotherListPromise;
})();

也可以使用 Promise.all():

(async () => {
  Promise.all([getList(), getAnotherList()]).then(...);
})();

async 继发和并发

// 继发一
async function loadData() {
  var res1 = await fetch(url1);
  var res2 = await fetch(url2);
  var res3 = await fetch(url3);
  return "whew all done";
}

// 继发二
async function loadData(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
// 并发一
async function loadData() {
  var res = await Promise.all([fetch(url1), fetch(url2), fetch(url3)]);
  return "whew all done";
}

// 并发二(for循环里面加计时器的案例)
async function loadData(urls) {
  // 并发读取 url
  const textPromises = urls.map(async url => {
    const response = await fetch(url);
    return response.text();
  });

  // 按次序输出
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}

async 错误捕获

// to.js
export default function to(promise) {
   return promise.then(data => {
      return [null, data];
   })
   .catch(err => [err]);
}
import to from './to.js';

async function asyncTask() {
     let err, user, savedTask;
     [err, user] = await to(UserModel.findById(1));
     if(!user) throw new CustomerError('No user found');

     [err, savedTask] = await to(TaskModel({userId: user.id, name: 'Demo Task'}));
     if(err) throw new CustomError('Error occurred while saving task');

    if(user.notificationsEnabled) {
       const [err] = await to(NotificationService.sendNotification(user.id, 'Task Created'));
       if (err) console.error('Just log the error and continue flow');
    }
}

async 的一些讨论

async 会取代 Generator 吗?

Generator 本来是用作生成器,使用 Generator 处理异步请求只是一个比较 hack 的用法,在异步方面,async 可以取代 Generator,但是 async 和 Generator 两个语法本身是用来解决不同的问题的。

async 会取代 Promise 吗?

  1. async 函数的就是返回一个 Promise 对象。他们两个相互成就,我觉得可以这样说。
  2. 面对复杂的异步流程,Promise 提供的all和 race 会更加好用。会避免掉进await的陷阱里
  3. Promise 本身是一个对象,所以可以在代码中任意传递,缓存。

co 模块自动执行器

co 模块 函数通过回调函数 / Promise对象来接收和交还程序的执行权。从而自动化执行生成器函数。co 模块交还和接收执行权的本质就是等待异步操作的协程有了结果之后,去递归调用传入的回调函数来归还异步函数协程的执行权。

手写 co 函数

const fs = require("fs");

const readFileFn = (filename) => {
  /* Promise交还执行权 */
  return new Promise((resolve, reject) => {
    fs.readFile(filename, (err, data) => {
      if (err) reject(err);
      resolve(data);
    });
  });

  /* 回调函数交还执行权 */
  return (cb) => {
    fs.readFile(filename, (err, data) => {
      if (err) throw err;
      setTimeout(() => {
        cb(data);
      });
    });
  };
};

function* generatorFn() {
  const r1 = yield readFileFn("./a.txt");
  const r2 = yield readFileFn("./b.txt");
  console.log(r1.toString(), r2.toString(), "两个文件读取完毕!!!");
}
const co = (geFn) => {
  const gen = geFn();
  const coRe = (data) => {
    const result = gen.next(data);
    if (result.done) return result.value;
    if (result.value instanceof Promise) {
      result.value.then(coRe);
    } else {
      result.value(coRe);
    }
  };
  coRe();
};
co(generatorFn);
// 第二版
function run(gen) {
    var gen = gen();

    return new Promise(function(resolve, reject) {

        function next(data) {
            try {
                var result = gen.next(data);
            } catch (e) {
                return reject(e);
            }

            if (result.done) {
                return resolve(result.value)
            };
            //统一回调函数和Promise
            var value = toPromise(result.value);

            value.then(function(data) {
                next(data);
            }, function(e) {
                reject(e)
            });
        }

        next()
    })

}

function isPromise(obj) {
    return 'function' == typeof obj.then;
}

function toPromise(obj) {
    if (isPromise(obj)) return obj;
    if ('function' == typeof obj) return thunkToPromise(obj);
    return obj;
}

function thunkToPromise(fn) {
    return new Promise(function(resolve, reject) {
        fn(function(err, res) {
            if (err) return reject(err);
            resolve(res);
        });
    });
}

module.exports = run;

协程概念

协程的概念是多个线程互相协作,完成异步任务。

  • 协程(Coroutines)是一种比线程更加轻量级的存在 , 协程是一个特殊的函数,这个函数可以暂停,保留状态后暂时推出,之后可以重新在暂停处恢复运行。

第一步,协程 A 开始执行。第二步,协程 A 执行到一半,进入暂停,执行权转移到协程 B。第三步,(一段时间后)协程 B 交还执行权。第四步,协程 A 恢复执行。 在 ES6 当中可以通过生成器函数可以交出函数的执行权的特性来实现协程。

生成器

生成器

  • 生成器是 ES6 的一种对函数执行进行控制的方案。它让我们更加灵活的控制函数什么时候继续执行,什么时候暂停执行。

生成器函数

  • 生成器函数通过  yield  关键字来控制函数执行流程。
  • 生成器函数返回生成器。
  • yield  之后的返回值是,next方法返回对象的  value  和  done  属性。
    • {value: , done: false}
  • para2  为 第二段函数代码的参数。para3  为 第三段函数代码的参数。
function* bar() {
  const val_1 = 1;
  console.log("第一段代码执行", val_1);
  const para2 = yield "第一段代码的返回值";
  const val_2 = 2;
  console.log("第二段代码执行", val_2 * value_2);
  const para3 = yield "第二段代码的返回值";
  const val_3 = 3;
  console.log("第三段代码执行", val_3 * value_3);
  yield "第三段代码的返回值";
  console.log("函数执行结束");
}
const generator = bar();
console.log(generator.next());
console.log(generator.next(10));
console.log(generator.next(10));
console.log(generator.next(10));
console.log(generator.next(10));
console.log(generator.next(10));
// 第一段代码执行 1
// { value: '第一段代码的返回值', done: false }
// 第二段代码执行 20
// { value: '第二段代码的返回值', done: false }
// 第三段代码执行 30
// { value: '第三段代码的返回值', done: false }
// 函数执行结束
// { value: undefined, done: true }
// { value: undefined, done: true }
// { value: undefined, done: true }

生成器提前结束 - return 函数

  • return 传值后这个生成器函数就会结束,之后调用 next 不会继续生成值了;
function* bar() {
  const val_1 = 1;
  console.log("第一段代码执行", val_1);
  const value_2 = yield "第一段代码的返回值";
  const val_2 = 2;
  console.log("第二段代码执行", val_2 * value_2);
  const value_3 = yield "第二段代码的返回值";
  const val_3 = 3;
  console.log("第三段代码执行", val_3 * value_3);
  yield "第三段代码的返回值";
  console.log("函数执行结束");
}
const generator = bar();
console.log(generator.next());
console.log(generator.return(10));
console.log(generator.next(10));
// 第一段代码执行 1
// { value: '第一段代码的返回值', done: false }
// { value: 10, done: true }
// { value: undefined, done: true }

生成器抛出异常 - throw 函数

  • 给生成器函数内部抛出异常
function* bar() {
  const val_1 = 1;
  console.log("第一段代码执行", val_1);
  const value_2 = yield "第一段代码的返回值";
  const val_2 = 2;
  console.log("第二段代码执行", val_2 * value_2);
  const value_3 = yield "第二段代码的返回值";
  const val_3 = 3;
  console.log("第三段代码执行", val_3 * value_3);
  yield "第三段代码的返回值";
  console.log("函数执行结束");
}
const generator = bar();
console.log(generator.next());
console.log(generator.throw("err message"));
console.log(generator.next(10));
//   const value_2 = yield "第一段代码的返回值";
//                   ^
// err message
// (Use `node --trace-uncaught ...` to show where the exception was thrown)

抛出异常后我们可以在生成器函数中捕获异常

function* bar() {
  const val_1 = 1;
  console.log("第一段代码执行", val_1);
  try {
    const value_2 = yield "第一段代码的返回值";
  } catch (err) {
    console.log("捕获了异常" + err);
  }
  const val_2 = 2;
  console.log("第二段代码执行", val_2);
  const value_3 = yield "第二段代码的返回值";
  const val_3 = 3;
  console.log("第三段代码执行", val_3 * value_3);
  yield "第三段代码的返回值";
  console.log("函数执行结束");
}
const generator = bar();
console.log(generator.next());
console.log(generator.throw("err message"));
console.log(generator.next(10));
// 第一段代码执行 1
// { value: '第一段代码的返回值', done: false }
// 捕获了异常err message
// 第二段代码执行 2
// { value: '第二段代码的返回值', done: false }
// 第三段代码执行 30
// { value: '第三段代码的返回值', done: false }

生成器代替迭代器

// 生成器
function* createGenerator(arr) {
  for (const item of arr) {
    yield item;
  }
}
const generator = createGenerator([1, 3]);

// 迭代器
function createIteratot(arr) {
  let index = 0;
  return {
    next: function () {
      if (index < arr.length) {
        return { value: arr[index++], done: false };
      } else {
        return { value: undefined, done: true };
      }
    },
  };
}

生成器原理

**function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}**

babel 当中有一个非常复杂的原型解构,实现了 next, return, throw 方法。

babel 当中的核心逻辑是,是有一个 while 无限循环,无限循环中有 switch case 语句,分为对应着 0 → hello,1 → world , 2 → ending 3 → .stop() 每一次 yield 都会匹配 switch 语句,然后去改变 next, pre 属性的值,执行下一个 yield 匹配,最后一个 yield 匹配后 最后一个状态会变为 complete , 会 return {value: undefined, done: true}

async 和 await 被 babel 编译后的结果就是, 通过 generator + co 自动化的递归的执行 _asyncToGenerator函数。 来实现 async/await

let | const | var

块级作用域:块作用域由 { }包括,let 和 const 是块级作用域的块级声明,var 不存在块级作用域。存在变量提升的问题。

变量提升:var 存在变量提升,let 和 const 不存在变量提升。var 的变量提升就是在初始化全局上下文对象的 VO 变量的时候,收集全局函数和全局变量。

暂时性死区:在使用 let、const 命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用 var 声明的变量不存在暂时性死区。

全局作用域:var 声明的变量为全局变量,并且会将该变量添加为全局对象的属性(浏览器环境下是 window,node 环境是 global),但是 let 和 const 不会,他们被添加到一个特殊的变量环境当中来记录。

重复声明:var 声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的变量,const 和 let 不允许重复声明变量。

Set WeakSet | Map Weakmap

  • Set 和 WeakSet 都是集合数据结构。结构中的每个元素都是唯一的。
  • 通过 add 增加值,has 查询,size 取得元素数量,delete 和 clear 删除元素。
  • WeakSet 只能使用对象作为值,不接受其他类型的值。
// set 数据结构的基本使用
const set = new Set();
set.add(10); // 返回set对象
set.add(10);
set.add({ name: 'ryan' });
console.log(set);
set.has(10); // 返回布尔值
set.delete(10); // 返回布尔值
console.log(set);
set.clear();
console.log(set);
console.log(set.size);
const set1 = new Set([1, 1, 3, 3, 0]); // 传入可迭代对象

// set的遍历
set.forEach((item) => {
  console.log(item);
});
for (const val of set) {
  console.log(val);
}
const set2 = new WeakSet();
// 注意这样添加的是不同对象。相同的对象 -> const obj = {name: 'ryan'} set.add(obj)
set.add({ name: 'ryan' }); // 返回set对象
set.add({ name: 'ryan' });
set.add({ name: 'ryan' });
console.log(set2);
set.has({ name: 'ryan' }); // 返回布尔值
set.delete({ name: 'ryan' }); // 返回布尔值
// console.log(set2);
// set.clear();
console.log(set2);
console.log(set.size);
const set3 = new WeakSet([{ name: 'ryan' }]); // 传入可迭代对象
console.log(set3);
// set -> []
const arr = Array.from(set2);
const arr2 = [...set2];

  • Map 和 WeakMap 都是 K V 表,映射数据结果。
  • 之前存储键值对,只能通过 Object,并且有个局限就是属性名只能是字符串类型 ,Symbol 数字类型。Map 的属性可以是任何 JS 类型。
  • 通过 set 添加属性, 通过 has 查询,get 获取属性,通过 delete, clear 删除属性。
  • WeakMap 只能使用对象作为键,不接受其他类型的值。
  • WeakMap 不可以遍历。也正是因为弱引用这样的特性,WeakMap 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakMap 不可遍历。
const map = new Map();
const obj1 = { name: 'why' };
const obj2 = { name: 'kobe' };
map.set(obj1, 'aaa');
map.set(obj2, 'bbb');
map.set(1, 'ccc');

map2.set('why', 'eee');
console.log(map2);
console.log(map2.get('why'));
console.log(map2.has('why'));

map2.delete('why');
console.log(map2);

map2.clear();
console.log(map2);
// 传入entry
const map2 = new Map([
  [obj1, 'aaa'],
  [obj2, 'bbb'],
  [2, 'ddd'],
]);

Dom | Bom

Bom

BOM(Browser Object Model) 浏览器对象模型看成是连接JavaScript 脚本与浏览器窗口的桥梁。

  • BOM 主要包括一下的对象模型:
    • window:包括全局属性、方法。
    • location:操作 和 访问 URL 信息的对象。
    • history:操作浏览器的会话历史记录的对象 📝。
    • document:操作文档对象。

window

window 作为全局对象

window.setTimeout(() => {
  console.log("setTimeout");
}, 2000);

const obj = new window.Date();

window 常见的属性,方法和事件

// 1.常见的属性
console.log(window.screenX);
console.log(window.screenY);

window.addEventListener("scroll", () => {
  console.log(window.scrollX, window.scrollY);
});

console.log(window.outerHeight);
console.log(window.innerHeight);

// 2.常见的方法
const scrollBtn = document.querySelector("#scroll");
scrollBtn.onclick = function () {
  // 1.scrollTo
  window.scrollTo({ top: 2000 });

  // 2.close
  window.close();

  // 3.open
  window.open("http://www.baidu.com", "_self");
};

// 3.常见的事件
window.onload = function () {
  console.log("window窗口加载完毕~");
};

window.onfocus = function () {
  console.log("window窗口获取焦点~");
};

window.onblur = function () {
  console.log("window窗口失去焦点~");
};

const hashChangeBtn = document.querySelector("#hashchange");
hashChangeBtn.onclick = function () {
  location.hash = "aaaa";
};
window.onhashchange = function () {
  console.log("has发生了h改变");
};

window 继承自 EventTarget

const clickHandler = () => {
  console.log("window发生了点击");
};

window.addEventListener("click", clickHandler);
window.removeEventListener("click", clickHandler);

window.addEventListener("a", () => {
  console.log("a");
});
// 派发事件
window.dispatchEvent(new Event("a"));

location

  • Location  对象用于表示  window  上当前链接到的 URL 信息。

location 方法

  • Location.assign()  方法会触发窗口加载并显示指定的 URL 的内容。
  • Location.replace()  方法以给定的 URL 来替换当前的资源。 与  assign()方法 不同的是,调用  replace()  方法后,当前页面不会保存到会话历史中(session History),这样,用户点击回退按钮时,将不会再跳转到该页面。
  • Location.reload()  方法用来刷新当前页面。该方法只有一个参数,当值为 true 时,将强制浏览器从服务器加载页面资源,当值为 false 或者未传参时,浏览器则可能从缓存中读取页面。

location 属性

  • 打开https://developer.mozilla.org/zh-CN/docs/Web/API
  • console.log(location)
  • hash: ""
  • host: "developer.mozilla.org"
  • hostname: "developer.mozilla.org"
  • href: "https://developer.mozilla.org/zh-CN/docs/Web/API"
  • origin: "https://developer.mozilla.org"
  • pathname: "/zh-CN/docs/Web/API"
  • port: ""
  • protocol: "https:"

history

  • history  对象允许我们访问浏览器曾经的会话历史记录。

history 属性

  • length:会话中的记录条数。
  • state:当前保留的状态值。

history 方法

  • back()返回上一页,等价于  history.go(-1)。
  • forward()前进下一页,等价于  history.go(1)。
  • go()加载历史中的某一页。
  • pushState();只是改变跳转路径不刷新网页。
  • replaceState()打开一个新的地址,并且使用  replace没有会话记录。

Dom

  • Node接口 继承了  EventTarget,而像  Document,Element  都继承自  Node  接口。
  • 继承 自 EventTarget 可以使用 addEventListener, removeEventListener, dispatchEvent。
  • 继承 Node 可以使用 nodeValue, nodeType, childNodes
  • element → classLIst className style, clientHeight, clientHeight
  • document → document.titile document.body, document.querySelector()

继承 自 EventTarget

  • addEventListener, removeEventListener, dispatchEvent。
// document
document.addEventListener("click", () => {
  console.log("document被点击");
});

const divEl = document.querySelector("#box");
const spanEl = document.querySelector(".content");
// element -> HTMLElement
divEl.addEventListener("click", () => {
  console.log("div元素被点击");
});

spanEl.addEventListener("click", () => {
  console.log("span元素被点击");
});

继承 Node

const divEl = document.querySelector("#box");
const spanEl = document.querySelector(".content");

// 常见的属性
console.log(divEl.nodeName, spanEl.nodeName);
console.log(divEl.nodeType, spanEl.nodeType);
console.log(divEl.nodeValue, spanEl.nodeValue);

// childNodes
const spanChildNodes = spanEl.childNodes;
const textNode = spanChildNodes[0];
console.log(textNode.nodeValue);

// 常见的方法
const strongEl = document.createElement("strong");
strongEl.textContent = "我是strong元素";
divEl.appendChild(strongEl);
document.body.appendChild(strongEl);

Document

// 常见的属性
console.log(document.body);
console.log(document.title);
document.title = "Hello World";

console.log(document.head);
console.log(document.children[0]);

console.log(window.location);
console.log(document.location);
console.log(window.location === document.location);
// true
const imageEl = document.createElement("img");
const imageEl2 = new HTMLImageElement();

// 获取元素
const divEl1 = document.getElementById("box");
const divEl2 = document.getElementsByTagName("div");
const divEl3 = document.getElementsByName("title");
const divEl4 = document.querySelector(".content");
const divEl5 = document.querySelectorAll(".content");

Element

const divEl = document.querySelector("#box");

// 常见的属性
console.log(divEl.id);
console.log(divEl.tagName);
console.log(divEl.children);
console.log(divEl.className);
console.log(divEl.classList);
console.log(divEl.clientWidth);
console.log(divEl.clientHeight);
console.log(divEl.offsetLeft);
console.log(divEl.offsetTop);

// 常见的方法
const value = divEl.getAttribute("age");
console.log(value);
divEl.setAttribute("height", 1.88);

事件监听(事件流)

事件流包含三个阶段:

  1. 事件捕获阶段
  2. 目标阶段
  3. 事件冒泡阶段

首先发生的是事件捕获,沿着  DOM  树一路向下,在经过的每个 dom 节点依次触发监听的事件。直到   目标 dom 节点  document -> html -> body -> div。

然后是实际的目标 dom 节点接收到事件,触发目标节点绑定事件/触发代理事件

最后阶段是事件冒泡阶段,沿着  DOM  树一路向上,在经过的每个 dom 节点依次触发监听的事件。直到  document  对象  div -> body -> html -> document。

<html>
    <body>
        <div>
            <button></button>
        </div>
    </body>
</html>tytyt

事件冒泡

  • 沿着  DOM  树一路向上,在经过的每个节点依次触发。直到  document  对象  div -> body -> html -> document。
spanEl.addEventListener("click", () => {
  console.log("事件冒泡:span元素被点击了");
});

divEl.addEventListener("click", () => {
  console.log("事件冒泡:div元素被点击了");
});

document.body.addEventListener("click", () => {
  console.log("事件冒泡:body元素被点击了");
});

document.addEventListener("click", () => {
  console.log("事件冒泡:document元素被点击了");
});

事件捕获

  • 沿着  DOM  树一路向下,在经过的每个节点依次触发。直到   目标对象  document -> html -> body -> div。
spanEl.addEventListener(
  "click",
  (event) => {
    console.log("事件捕获:span元素被点击了");
    event.stopPropagation();
  },
  true
);

divEl.addEventListener(
  "click",
  () => {
    console.log("事件捕获:div元素被点击了");
  },
  true
);

document.body.addEventListener(
  "click",
  (event) => {
    console.log("事件捕获:body元素被点击了");
  },
  true
);

事件代理

事件代理又称之为事件委托, 事件代理是把原本需要绑定在子元素的事件委托给父元素,让父元素负责事件监听和处理。

事件代理的好处是有两点:

第一点:可以大量节省内存占用,减少事件注册事件。

第二点:当新增子对象时无需再次对其绑定

为什么父元素能做到事件代理呢? 笔者认为有两点:

第一点 :事件冒泡到父元素,父元素可以订阅到冒泡事件。

第二点:可以通过 event.target 得到目标节点。不然, 父元素怎么针对不同的子节点,进行定制化事件代理。

事件对象的属性和方法

  • eventTarget  和·currentTarget  的区别:
    • 点击父元素中的子元素。
    • eventTarget | target  指向子元素,currentTarget  指向父元素。
  • 事件委托,向父元素上监听事件来操作子元素。
divEl.addEventListener('click', (event) => {
  console.log('span元素被点击:', event);
  console.log('事件的类型:', event.type);
  console.log('事件的元素:', event.target, event.currentTarget);
  console.log('事件发生的位置:', event.offsetX, event.offsetY);
});

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...