学习专题

对象继承

  1. 原型链继承(引用值共享的问题)
  2. 构造函数继承(没有办法拿到原型上的方法)
  3. 组合继承(伪经典继承)
  4. 寄生组合继承(经典继承)
  5. 圣杯模式(Buffer)
  6. extends继承(es6)
  7. 拷贝继承

原型链继承

缺点:引用值共享

  1. //实例对象sub1有能力访问自身的原型 sub1 -> Sub.prototype
  2. //Sub.prototype = new Super()
  3. //new Super()实例对象 有能力访问自身的原型 new Super -> Super.prototype
  4. function Super() {
  5. this.a = '111';
  6. }
  7. Super.prototype.say = function () {
  8. console.log('222');
  9. }
  10. function Sub() {
  11. }
  12. Sub.prototype = new Super();
  13. var sub1 = new Sub();
  14. console.log(sub1.a); //111
  15. console.log(sub1.say()); //222

引用值共享问题

  1. function Super() {
  2. this.a = [1, 2, 3, 4];
  3. }
  4. Super.prototype.say = function () {
  5. console.log('222');
  6. }
  7. function Sub() {
  8. }
  9. Sub.prototype = new Super();
  10. var sub1 = new Sub();
  11. var sub2 = new Sub();
  12. sub1.a.push(5);
  13. //按道理两个实例化对象不会影响属性
  14. console.log(sub1.a); //[1, 2, 3, 4, 5]
  15. console.log(sub2.a); //[1, 2, 3, 4, 5]

构造函数继承

  1. //构造函数继承
  2. //解决引用值共享的问题
  3. function Super() {
  4. this.a = [1, 2, 3, 4];
  5. }
  6. Super.prototype.say = function () {
  7. console.log('222');
  8. }
  9. function Sub() {
  10. var that = this;
  11. // this.a = [1, 2, 3, 4];
  12. Super.call(that);
  13. }
  14. var sub1 = new Sub();
  15. var sub2 = new Sub();
  16. //更改原始值
  17. // sub1.a = '333';
  18. //更改引用值
  19. sub1.a.push(5);
  20. //测试
  21. console.log(sub1.a);
  22. console.log(sub2.a);

组合继承(伪经典继承)

  1. //组合继承
  2. function Super() {
  3. this.a = [1, 2, 3, 4];
  4. }
  5. Super.prototype.say = function () {
  6. console.log('222');
  7. }
  8. //借用
  9. function Sub() {
  10. var that = this;
  11. //这里会重复调用Super
  12. Super.call(that);
  13. }
  14. //原型链继承
  15. Sub.prototype = new Super();
  16. var sub1 = new Sub();
  17. var sub2 = new Sub();
  18. //测试
  19. console.log(sub1.say());
  20. console.log(sub2.say());

寄生组合继承(经典继承)

缺点:有机会被重写导致无法访问自身原型上的属性和方法

  1. //寄生组合继承(经典继承)
  2. //Object.create()
  3. /**
  4. * es5以下不支持
  5. * 兼容性方案
  6. * @参数 目标对象的原型
  7. * @返回值 新的继承后的实例化对象
  8. */
  9. if (!Object.create) {
  10. Object.create = function (proto) {
  11. function F() {}
  12. F.prototype = proto.prototype;
  13. return new F();
  14. }
  15. }
  16. function Super() {
  17. this.a = [1, 2, 3, 4];
  18. }
  19. Super.prototype.say = function () {
  20. console.log('222');
  21. }
  22. //借用
  23. function Sub() {
  24. var that = this;
  25. //这里会重复调用Super
  26. Super.call(that);
  27. }
  28. //如果在Sub.prototype赋值前定义方法会被覆盖掉
  29. //缺点:Sub.prototype自身的属性和方法无法访问
  30. Sub.prototype.say = function(){
  31. console.log('444')
  32. }
  33. //API继承Super原型
  34. Sub.prototype = Object.create(Super.prototype);
  35. var sub1 = new Sub();
  36. var sub2 = new Sub();
  37. // //更改原始值
  38. // sub1.a = '333';
  39. // //更改引用值
  40. // sub1.a.push(5);
  41. //测试
  42. // console.log(sub1.a);
  43. // console.log(sub2.a);
  44. // console.log(sub1.say());
  45. // console.log(sub2.say());

toString

Object.prototype.toString.call()

3种判断数组的方法:

  • constructor
  • instanceof:不能判断原始值
  • Object.prototype.toString
  1. var a = [] || {};
  2. console.log(a.constructor); //Array(){}
  3. console.log(a instanceof Array); //true
  4. var string = Object.prototype.toString.call(a);
  5. console.log(string); //[object Array]
  6. Object.prototype = {
  7. toString: function () {
  8. //call(a) -> a.toString()
  9. this.toString(); //[object Array]
  10. }
  11. }
  1. var arr = new Array(1, 2, 3);
  2. console.log(arr.toString()); //1 2 3
  3. console.log(arr); //[1, 2, 3]
  4. console.log(Object.prototype.toString.call(arr)); //[object Array]

以上推出Object.prototype.toString.call()结果的用法,此方法可以判断后端发送过来的数据的数据类型

  1. var str = Object.prototype.toString;
  2. var trueTip = '[object Array]';
  3. if (str.call(a) === trueTip) {
  4. console.log('是数组'); //是数组
  5. } else {
  6. console.log('不是数组');
  7. }

预加载

预加载:等图片加载完毕才放进html

  1. //预加载
  2. var oDiv = document.getElementsByTagName('div')[0];
  3. var imgSrcArr = [
  4. 'http://www.xxx.com/1.jpg',
  5. 'http://www.xxx.com/1.jpg',
  6. 'http://www.xxx.com/1.jpg'
  7. ];
  8. imgSrcArr.forEach(function (elem) {
  9. var oImg = new Image();
  10. oImg.src = elem;
  11. oImg.style.width = '100%';
  12. //图片加载完毕插入到HTML
  13. oImg.onload = function () {
  14. oDiv.appendChild(oImg);
  15. }
  16. });

懒加载

懒加载:滚动页面加载图片,不滚动时不会加载图片

源码地址:https://gitee.com/kevinleeeee/img-lazy-load-demo

  1. function imgLazyLoad(images) {
  2. var imgLen = images.length,
  3. n = 0;
  4. return function () {
  5. // 找到可视区域高度
  6. var cHeight = document.documentElement.clientHeight,
  7. //找到滚动高度并兼容IE
  8. sTop = document.documentElement.scrollTop || document.body.scrollTop,
  9. imgItem;
  10. for (var i = n; i < imgLen; i++) {
  11. imgItem = images[i];
  12. //每张图片的offsetTop < cHeight + sTop证明出现在可视区域之内
  13. if (imgItem.offsetTop < cHeight + sTop) {
  14. //拿到当前项地址赋值给src
  15. imgItem.src = imgItem.getAttribute('data-src');
  16. //清除原来的data-src属性
  17. imgItem.removeAttribute('data-src');
  18. n++;
  19. }
  20. }
  21. }
  22. }

异步加载

JavaScript脚本 是由浏览器帮忙设置的

目的:为了不占用加载页面的时间

怎么设置异步加载脚本?

怎么写一个异步加载程序?

什么时候要用异步加载?

工具类函数/跟DOM不相关的库(里面的函数在需要调用的时候才执行)/按需加载

动态创建script标签什么时候开始下载文件?

document.createElement('script').src的时候已经开始下载文件

什么时候才能正确的执行下载的文件?

当创建的脚本放入到html里面的时候才开始执行

关于defer:

注:IE8及以下使用

标签属性,让标签变成异步加载另开辟其他线程,不影响css/dom解析

特点:

异步加载不阻塞但不会立即执行,等dom树构建解析完毕才执行

关于async

注:

  • w3c标准,html5IE9及以上支持
  • 异步加载操作时 不能进行DOM文档操作

标签属性 async="async"

自己写一个异步加载:

  1. //企业级异步加载写法
  2. //主动创建script就会变成异步来加载
  3. //此写法会阻塞window.onload()
  4. //window.onload()会等所有程序加载完毕才会执行,是弊端
  5. <script type="text/javascript">
  6. var s = document.createElement('script');
  7. s.type = 'text/javascript';
  8. s.src = 'utils.js';
  9. document.body.appendChild(s);
  10. // console.log(s);
  11. // <script type="text/javascript" src="utils.js"></script>
  12. </script>

问题:能否在页面onload全部加载完毕的情况下再进行异步加载呢?

  1. //希望onload完成之后再进行异步加载
  2. //一般来说,异步加载的<script>标签都放在head里面的上方
  3. (function () {
  4. function async_load() {
  5. var s = document.createElement('script'),
  6. //找到第一个<script>标签
  7. oScript = document.getElementsByTagName('script')[0];
  8. s.type = 'text/javascript';
  9. //异步加载该文件
  10. s.src = 'utils.js';
  11. //动态把新建的<script>标签插入到第一个找到<script>标签前面
  12. oScript.parentNode.insertBefore(s, oScript);
  13. }
  14. //事件处理的方式使当出发onload事件后,再执行异步加载的函数程序
  15. //兼容IE写法
  16. if (window.attachEvent) {
  17. window.attachEvent('onload', async_load);
  18. } else {
  19. window.addEventListener('load', async_load, false);
  20. }
  21. })();

防抖节流

函数防抖解决:

  1. 对于在事件被触发n秒后再执行的回调(延迟执行)
  2. 如果在这n秒内再次触发事件,计时器频繁重新开始计时

防抖存在问题:

  1. 污染全局
  2. 初次触发事件时,会延迟执行
  1. //封装
  2. //fn函数
  3. //time延迟时间
  4. //是否为第一次访问
  5. function debounce(fn, time, triggleNow){
  6. var t = null,
  7. res;
  8. //希望返回新的函数
  9. //万一返回的函数带有参数,带上参数args
  10. var debouced = function(){
  11. var _self = this,
  12. args = arguments;
  13. if(t){
  14. clearTimeout(t);
  15. }
  16. if(triggleNow){
  17. //exec是否执行
  18. //!null = true
  19. var exec = !t;
  20. //延迟执行
  21. t = setTimeout(function(){
  22. t = null;
  23. });
  24. //说明是首次访问
  25. if(exec){
  26. //直接执行
  27. res = fn.apply(_self, args);
  28. }
  29. }else{
  30. t = setTimeout(function(){
  31. res = fn.apply(_self, args);
  32. }, time);
  33. }
  34. return res;
  35. }
  36. //强制取消防抖
  37. debounced.remove = fuction(){
  38. clearTimeout(t);
  39. t = null;
  40. }
  41. return debounced;
  42. }

函数节流解决:

  • 事件被触发,n秒之内只执行一次处理函数
  1. function throttle(fn, delay){
  2. var t = null,
  3. begin = new Date().getTime();
  4. return function(){
  5. var _self = this,
  6. args = arguments,
  7. cur = new Date().getTime();
  8. clearTimeout(t);
  9. if(cur - bigin >= delay){
  10. fn.apply(_self, args);
  11. begin = cur;
  12. }else{
  13. t = setTimeout(function(){
  14. fn.apply(_self, args);
  15. }. delay);
  16. }
  17. }
  18. }

放大模式

模块化的放大模式(augmentation):把依赖的模块注入到程序中

好处:多人共同写同一模块

  1. var mod1 = (function (mod) {
  2. var test1 = function () {};
  3. var test2 = function () {}
  4. return {
  5. test1,
  6. test2
  7. }
  8. })(mod2);

bind/call/apply

重写call

特点:

  1. 如果一个函数跟上call(),说明函数执行
  2. 函数的this指向call的第一个参数
  3. call第2个参数开始是函数的参数列表
  1. function test(){
  2. console.log(this);
  3. //{a:1, b:2}
  4. console.log(arguments);
  5. //Arguments['zhangsan', 'lisi']
  6. }
  7. test.call({a:1, b:2}, 'zhangsan', 'lisi');

注意:ES6模块化中自动使用严格模式

  1. //非严格模式底下
  2. function test(){
  3. console.log(this);
  4. //this -> window
  5. }
  6. //严格模式下
  7. //this -> undefined

对于一个方法来说,谁调用它,默认函数内部的this指向就指向谁

  1. var obj = {
  2. a: 1,
  3. b: 2,
  4. test(){
  5. console.log(this);
  6. //this -> obj
  7. }
  8. }
  9. obj.test();
  1. window.test = function(){
  2. console.log(this);
  3. //this -> window
  4. }
  5. function test(){
  6. console.log(this);
  7. //this -> window
  8. }
  9. window.test();

问题:如何实现改变this指向?

默认情况: 函数内部this指向调用者

  1. myCall函数内部的this指向调用者函数本身
  2. 将第一个参数ctx对象里定义一个新的函数originFn
  3. myCall函数内部的thisctx.originFn的函数指向同一个引用地址
  4. 这样就可以使originFn指向调用者ctx
  5. 从而实现内部this指向第一个参数ctx对象

问题:如何拿到第二参数开始到结束的所有参数作为调用函数的实参列表?

  1. myCall函数内部定义args数组容器
  2. 从第一项开始for循环arguments里的参数
  3. 收集每一项到args数组里
  4. 利用eval()执行字符串程序eval("ctx.originFn(" + args + ")")
  5. args收集的参数展开平铺到fn的实参中去
  6. 实现拿到调用函数的实参列表
  7. 最后返回evel()方法的结果到外界实现调用函数会有返回值

源码:

https://gitee.com/kevinleeeee/my-call-vite-demo

重写apply

特点:

  1. 如果一个函数跟上apply(),说明函数执行
  2. 函数的this指向apply的第一个参数
  3. apply第2个参数开始是函数的数组参数列表
  4. apply只能取到第二个参数,第三个参数开始到最后忽略
  5. apply第二个参数传递null,undefined不报错但argument长度为0
  6. apply第二个参数是原始值时会报错
  7. apply第二个参数是对象或函数时arugument长度为0

源码地址:

https://gitee.com/kevinleeeee/my-apply-vite-demo

重写bind

特点:

  1. 如果一个函数跟上bind(),说明函数不执行
  2. 返回一个新的函数, 执行新的函数会改变this, 传参可以拿到参数
  3. bind可以分离调用函数的参数
  4. bind接受一部分参数,返回的新函数接受一部分参数
  5. 函数的this指向bind的第一个参数
  6. bind第2个参数开始是函数的参数列表
  7. 实例化返回的新函数this指向是调用函数构造的实例
  8. 实例应该继承构造函数上的原型属性
  1. var obj1 = { a: 1, b: 2 };
  2. var t = test.bind(obj1, 'rose', 'jack');
  3. //返回的新函数`this`指向是调用者
  4. t();
  5. //{a: 1, b: 2} Arguments(2) ['rose', 'jack', callee: (...), Symbol(Symbol.iterator): ƒ]
  6. //实例化返回的新函数`this`指向是调用函数构造的实例
  7. new t();
  8. //test {} Arguments(2) ['rose', 'jack', callee: (...), Symbol(Symbol.iterator): ƒ]
  1. //实例应该继承构造函数上的原型属性
  2. test.prototype.myLove = 'lisi';
  3. var t = test.bind(obj1, 'rose', 'jack');
  4. new t();
  5. //print:
  6. test {}
  7. [[Prototype]]: Object
  8. myLove: "lisi"

源码地址:

https://gitee.com/kevinleeeee/my-bind-vite-demo

new

案例:重写new

对实例化构造函数的new关键词进行重写

关于实例化的过程:

  1. function C(){
  2. this.a = a;
  3. this.b = b;
  4. }
  5. new C();
  6. //1.this指向一个空对象
  7. //2.在this.a/this.b时,会往空对象里增加{a: a, b: b}
  8. //3.空对象里还增加一个原型属性{__proto__: C.prototype}
  9. //4.c.prototype{ constructor: ƒ C()}

问题:如何模拟实例化过程?

new关键字无法模拟,在实例化时传入的参数1是构造函数, 其它参数顺位传入

  1. var test = myNew(Test, 1, 2);

源码地址:

https://gitee.com/kevinleeeee/my-new-vite-demo

循环

循环遍历迭代的区别

  • 循环:语言层面的语法 重复执行一段程序的方案
  • 遍历:业务层面的做法 观察或获取集合中的元素的一种做法
  • 迭代:实现层面的上的概念 实现遍历的底层方案其实就是迭代

ECMAScript3中,没有针对可迭代对象的具体的遍历方法,所以在ECMAScript5中增加:

  • 7个专门针对数组的遍历方法
  • 一个for..in对象的遍历方法

for in用于遍历对象或类数组拿到可枚举的键名,它的作用是用于调试,更方便的去检测对象的属性

for of只用于可迭代对象(Array,Map,Set,String,TypedArray,arguments等),它依赖Symbol.Iterator,创建一个迭代循环,调用自定义迭代钩子,为每个不同的属性的值执行语句

Symbol.Iterator为每一个对象定义默认的迭代器,迭代器可以被for of循环使用,注意对象原型上没有该迭代器

对象操作

EMCAScript委员会 14种底层对象操作方法(需要背)

方法一:获取原型

内置方法[[GetPrototypeOf]]

  1. var proto = Object.getPrototypeOf(obj);
  2. console.log(proto); 通过底层API方法获取
  3. console.log(obj.__proto__); 通过对象本身的原型容器获取
  4. console.log(Object.protype); 通过对象原型获取

方法二:设置原型

内置方法[[SetPrototypeOf]]

  1. Object.setPrototypeOf(obj, {c: 3, d: 4});
  2. console.log(obj);
  3. /**
  4. * {
  5. * a: 1,
  6. * b: 2,
  7. * __proto__:
  8. * c: 3,
  9. * d: 4
  10. * }
  11. */

方法三:获取对象的可拓展性

内置方法[[IsExtensible]]

  1. var extensible = Object.isExtensible(obj);
  2. console.log(extensible); //true

方法四:获取自有属性

内置方法[[GetOwnProperty]]

  1. Object.setPrototypeOf(obj, {c: 3, d: 4});
  2. console.log(Object.getOwnPropertyNames(obj));
  3. //['a', 'b'] 返回属性集合数组

方法五:禁止拓展对象

内置方法[[PreventExtensions]]

  1. Object.preventExtensions(obj);
  2. obj.c = 3; //禁止增加属性
  3. delete obj.a; //可删除属性
  4. console.log(obj); //对象没有变化

方法六:拦截对象操作

内置方法[[DefineOwnProperty]]

  1. Object.defineProperty(obj);

方法七:判断是否是自身属性

内置方法[[HasPropert]]

  1. console.log(obj.hasOwnPropert('a')); //true

方法八: 获取

内置方法[[Get]]

  1. console.log('a' in obj); //true
  2. console.log(obj.a); //1

方法九:设置

内置方法[[Set]]

  1. obj.a = 3;
  2. obj['b'] = 4;
  3. console.log(obj);

方法十:删除

内置方法[[Delete]]

  1. delete obj.a;
  2. console.log(obj);

方法十一:枚举

内置方法[[Enumerate]]

  1. for(var k in obj){
  2. console.log(obj[k]); //1 2 可枚举
  3. }

方法十二:获取键集合

内置方法[[OnPropertyKeys]]

  1. console.log(Object.keys(obj));
  2. //['a', 'b'] 返回属性集合数组

方法十三:函数调用

  1. function test(){...}
  2. test();
  3. obj.test2 = function(){...}
  4. obj.test2();

方法十四:实例化

  1. function Test(){...}
  2. new Test();

ES5方法重写

问题:为什么需要重写ES5中的方法?

在老的项目中(国家单位,银行,政府网站等)是在低版本IE浏览器里用ES3实现的,对ES5不兼容,所以要写成ES3能兼容的方法,能在老的项目中跑起来

需要重写的方法有:

  • forEach
  • map
  • filter
  • reduce
  • reduceRight
  • every
  • some
  • deepClone:重写以上方法时需要用到

源码地址: