js 基础

663 阅读26分钟

Ajax不支持浏览器back

ajax有一个明显的缺点就是与浏览器的 history.back() 与 forward() 不友好,不能前进与后退。比如分页加载,页面调走后在返回请求的分页数据就不在了。

  • 每次手动点击左侧的菜单,将 Ajax 地址的查询内容(?后面的)附在 demo HTML 页面地址后面,使用 history.pushState 塞到浏览器历史中。
  • 浏览器的前进与后退,会触发 window.onpopstate 事件,通过绑定 popstate 事件,就可以根据当前 UR L地址中的查询内容让对应的菜单执行 Ajax 载入,实现 Ajax 的前进与后退效果。
  • 页面首次载入的时候,如果没有查询地址、或查询地址不匹配,则使用第一个菜单的 Ajax 地址的查询内容,并使用 history.replaceState 更改当前的浏览器历史,然后触发Ajax操作。

$(document).ready 和 window.onload 的区别

  • ready 事件在DOM结构绘制完成之后就会执行。
  • load 事件必须等到网页中所有内容全部加载完毕之后才被执行。
  • DOM 文档加载步骤:
    • 解析HTML结构
    • 加载外部的脚本和样式文件
    • 解析并执行脚本代码
    • 执行 $(document).ready 内对应代码
    • 加载图片等二进制资源
    • 页面加载完毕执行 window.onload

css 放顶部 js 放底部

  • href:
    • 表示超文本引用,用来建立当前元素和文档间的链接
    • 常用的有 link, a
    • 异步加载 css
    • css 放顶部,在加载 html 生成 DOM tree 的时候,就可以同时对 DOM tree 进行渲染
  • src
    • 是资源,页面必不可少的一部分,src 指向的内容会嵌入到文档中当前标签的位置
    • 有 img, script,iframe
    • 同步加载,阻塞浏览器解析
    • js 加载后会立即执行,同时会阻塞后面的资源加载
    • js 可能会改 变DOM tree 的结构,所以需要一个稳定的 DOM tree
  • CSS 不会阻塞 DOM 的解析,但会阻塞 DOM 渲染
    • JS 阻塞 DOM 解析,但浏览器会"偷看"DOM,预先下载相关资源
    • 浏览器遇到 <script>且没有 defer 或 async 属性的标签时,会触发页面渲染,因而如果前面 CSS 资源尚未加载完毕时,浏览器会等待它加载完毕在执行脚本

为script tag 定义的 asyncdefer 属性有什么用

  • defer
    • 延迟加载,元素解析完成后执行
    • 会在 document 的 DOMContentLoaded 之前执行
    • 脚本在页面解析完毕之后,按照原本的顺序执行,所以如果前后有依赖关系的 js 可以放心使用
  • async
    • 异步加载,加载完毕立即执行,执行时会阻塞元素渲染
    • 在 window 的 load 事件之前执行
    • 由于加载完毕立即执行,可能出现脚本执行顺序被打乱的情况,如果 js 前后有依赖性,用 async,就很有可能出错
  • 相同点:
    • 加载文件时不阻塞页面渲染
    • 对于 inline 的 script(内联脚本)无效
    • 使用这两个属性的脚本中不能调用 document.write 方法、有脚本的 onload 的事件回调

json 和xml 数据的区别

  1. 数据体积方面:xml 是重量级的,json 是轻量级的,传递的速度更快些。
  2. 数据传输方面:xml 在传输过程中比较占带宽,json 占带宽少,易于压缩。
  3. 数据交互方面:json 与 js 的交互更加方便,更容易解析处理,更好的进行数据交互
  4. 数据描述方面:json 对数据的描述性比 xml 较差
  5. xml 和 json 都用在项目交互下,xml 多用于做配置文件,json 用于数据交互。

防抖与节流

1. 防抖 (debounce)

将多次高频操作优化为只在最后一次执行

  • 按钮提交场景 - 防止多次提交按钮,只执行最后一次提交
  • 服务端验证场景 - 表单验证(搜索联想词功)需要服务端配合,只执行一段连续输入事件的最后一次
function debounce(fun, delay) {
    let timer = null
    return function (...args) {
        timer && clearTimeout(timer)
        timer = setTimeout(() => {
            fun.call(this, ...args)
        }, delay)
    }
}

2. 节流(throttle)

每隔一段时间后执行一次,也就是降低频率,将高频操作优化成低频操作

  • 拖拽场景 - 固定时间内只执行一次,防止超高频次触发位置变动
  • 缩放场景 - 监控浏览器resize
  • 动画场景 - 避免段时间内多次触发动画引起性能问题
  • 滚动加载场景 - 加载更多或滚动时触发曝光
function throttle(func, delay) {
    let last = 0
    return function (...args) {
        let curr = +new Date()
        if (curr - last > delay){
            func.call(this, ...args)
            last = curr 
        }
   }
}

3. 增强版防抖

function debounce_throttle(fun, delay) {
    let timer = null
    let last = 0
    return function(...args) {
        let curr = +new Date()
        if (curr - last < delay) {
            timer && clearTimeout(timer)
            timer = setTimeout(() => {
                last = curr
                fun.call(this, ...args)
            }, delay)
        }
        else {
            last = curr
            fun.call(this, ...args)
        }
    }
    
}

4. requestAnimationFrame

requestAnimationFrame 是在浏览器中排队等待执行的一种方法,它可以在呈现性能的最佳时间执行。一个函数被 requestAnimationFrame 放入队列后将会在下一帧触发。浏览器会努力确保每秒 60 帧(60fps)。然而,如果浏览器无法确保,那么自然会限制每秒的帧数。例如,某个设备可能只能处理每秒 30 帧,所以每秒只能得到 30 帧。使用 requestAnimationFrame 来节流是一种有用的技术,它可以防止在一秒中进行 60 帧以上的更新。如果一秒钟内完成 100 次更新,则会为浏览器带来额外的负担,而用户却无法感知到这些工作。

函数式编程

JavaScript 柯里化

1. 概念

函数式编程是一种编程风格,它可以将函数作为参数传递,并返回没有副作用(改变程序状态)的函数。

  • 函数式编程的主要思想是把运算过程尽量写成一系列嵌套的函数调用
  • 函数可以赋值给其他变量,可以作为参数,可以作为别的函数的返回值
  • 引用透明即是纯函数即没有副作用

2. 优势

  • 大量使用函数,减少了代码的重复,因此程序比较短,开发速度快;
  • 接近自然语言易于理解;
  • 没有副作用,可以被看作独立单元,利于进行单元测试和除错以及模块化组合;
  • 易于并发编程;代码的热升级;

3. 函数柯里化

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

比较多次接受的参数总数与函数定义时的入参数量,当接受参数的数量大于或等于被 Currying 函数的传入参数数量时,就返回计算结果,否则返回一个继续接受参数的函数。

const _curry = (fn, ...args) => {
    if(args.length >= fn.length) {
        return fn(...args)
    }
    return function(...args2) {
        return _curry(fn, ...args, ...args2)
    }
}
const curry = fn => {
  const _c = (restNum, argsList) => restNum === 0 ?
    fn(...argsList) : x => _c(restNum - 1, [...argsList, x]);
  return _c(fn.length, []);
}

js中的事件

1. js事件模型

JS的事件模型就是一种观察者模式的体现,当对应的事件被触发时,监听该事件的所有监听函数都会被调用

2. DOM事件流分为三个阶段:

捕获阶段、目标阶段、冒泡阶段。先调用捕获阶段的处理函数,然后调用目标阶段的处理函数,最后调用冒泡阶段的处理函数。

  • 阻止事件的传播:event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true
  • 阻止事件默认行为: event.preventDefault ? event.preventDefault() : event.returnValue = false
  • addEventListener(event, listener, useCapture)
    • event---(事件名称,如click,不带on)
    • listener---事件监听函数
    • useCapture---是否采用事件捕获进行事件捕捉,默认为false,即采用事件冒泡方式

3. 事件委托以及优缺点

js事件委托就是利用冒泡的原理,把本应该添加到某个元素上的事件委托给他的父级,可以大量节省内存占用,减少事件注册。比如ul上代理所有li的click事件就很不错。可以实现当新增子对象时,无需再对其进行事件绑定,对于动态内容部分尤为合适

事件代理的常用应用应该仅限于上述需求,如果把所有事件都用事件代理,可能会出现事件误判。即本不该被触发的事件被绑定上了事件。

4. callee与caller,apply与call的区别和作用

  • Callee /kɔ:'li/ 是arguments的一个属性,指向调用arguments的函数自身,常见在递归匿名函数或者保证函数的封装性。
  • Caller /'kɔ:lə/ 函数对象的一个属性,指向调用当前函数的函数。若fn是在全局作用域下调用的 则 返回null。
  • fn.call(thisObj,arg1,arg2..) 函数作为另一个对象的方法被调用第一个参数后为参数列表
  • fn.apply(thisObj,arguments) 函数作为另一个对象的方法被调用第二个参数为数组

模块化

CommonJS 和 Node.js

Node.js 是 commonJS 规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global。

commonJS 用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。

1. 定义模块 math.js

使用 module.exports 定义当前模块对外输出的接口(不推荐直接用exports)

var basicNum = 0;
function add(a, b) {
  return a + b;
}
module.exports = {
  add: add,
  basicNum: basicNum
}

2. 引用模块

引用自定义的模块时,参数包含路径,可省略.js

var math = require('./math');
math.add(2, 5);

引用核心模块时,不需要带路径

var http = require('http');
http.createService(...).listen(3000);

AMD 和 require.js

require.js 是 AMD 规范的主要实践者,推崇依赖前置、提前执行。采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

1. 定义模块 math.js

如果我们定义的模块本身也依赖其他模块,那就需要将它们放在[]中作为define()的第一参数中

define(['underscore'], function(_){
  var classify = function(list){
    _.countBy(list, function(num){
      return num > 30 ? 'old' : 'young';
    })
  };
  return {
    classify :classify
  };
})

2. 引用模块

引用模块时,将模块名放在[]中作为reqiure()的第一参数

require(['jquery', 'math'], function($, math){
  var sum = math.add(10,20);
  $("#sum").html(sum);
});

3. 引用模块的路径

用require.config()指定引用路径

require.config({
  baseUrl: "js/lib",
  paths: {
    "jquery": "jquery.min",  //实际路径为js/lib/jquery.min.js
    "underscore": "underscore.min",
  }
});

CMD 和 sea.js

sea.js 是 CMD 规范的主要实践者,推崇依赖就近、延迟执行。

1. 定义模块 math.js

define(function(require, exports, module) {
    var $ = require('jquery.js');
    var add = function(a,b){
        return a+b;
    }
    exports.add = add;
});

2. 定义模块 math.js

seajs.use(['math.js'], function(math){
    var sum = math.add(1+2);
});

ES6 Module

1. 定义

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS(服务器) 和 AMD(浏览器)。ES6 在语言标准的层面上,实现了模块功能,旨在成为浏览器和服务器通用的模块解决方案。export 命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能。

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量,所以无法实现条件加载。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。

2. ES6 模块与 CommonJS 模块的差异

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
    • CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值
    • ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
    • 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”
    • 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import 时采用静态命令的形式。即在 import 时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”
  3. 总结
    • CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成
    • ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成

错误捕获

JS 异步错误捕获

同步错误和异步错误

1. 同步错误的捕获

try...catch 块可以捕获相同执行环境的错误,即同步错误

try {
    throw new Error('async error')
} catch(e) {
    console.log(e, 'err')
}

2. 异步错误的捕获

try...catch 无法捕获异步错误跟浏览器的执行机制有关。异步任务的回调函数由 eventloop 加入任务队列,回调函数执行的时候调用方法的执行栈已经退出,上下文环境已经改变,所以无法捕获错误。async 结合 promise 去处理 js 的异步错误会是比较方便的

// setTimeout 的回调函数抛出一个错误,并不会在 catch 中捕获
try {
    setTimeout(() => {
        throw new Error('async error')
    }, 1000)
} catch(e) {
    console.log(e, 'err')
}

3. Promise 的异常捕获

  • 构造函数
    • Promise 构造函数中 throw 的错误是同步执行的,但是不会被冒泡出来而是被 promise 吃掉了,只有通过 promise.catch 才可以捕获
    • Promise 构造函数中 reject 的错误,也可以被 catch 捕获
  • then 的回调的错误只能是在回调函数内部 catch 错误,并把错误信息返回,error 会传递到下一个 then 的回调
  • 用 Promise 捕获异步错误:把异步操作用 Promise 包装,通过内部判断把错误 reject,在外面通过 promise.catch 捕获
const p3 = () =>  new Promise((reslove, reject) => {
  setTimeout(() => {
    reject('async error');
  })
});
p3().catch(e => console.log(e));

4. async/await 的异常捕获

async 函数内部配合使用了 Promise,会根据 await 、 catch 等关键字被编译成好三段(1. fetchFailure 2. console.log(res) 3. catch

  • 从 1->2,异步是通过 Promise.then() 控制的
  • 当 Promise 的状态 reject 后,会向上冒泡直到('throw') 执行
  • 然后 catch 里的代码执行
try {
    const res = await fetchFailure();
    console.log(res, 'res');
} catch(e) {
    console.log(e, 'e.message');
}

处理异常代码整理

1. 定义各种各样的错误类型

class ValidatedError extends Error {
    public errmsg: string;
    public errno: number;
    constructor(msg: string, code: number) {
        super(msg);
        this.errmsg = msg || 'validated_error_msg';
        this.errno = code || 20010;
    }
}

2. 错误处理的逻辑

按功能需求来划分:请求失败(200 但是返回值有错误信息),node 中写 db 失败等

const errorHandle = (e: Error) => {
    // do something
    if(e instanceof ValidatedError) {
        // do sth
        return e;
    }
    return {
        code: 101,
        errmsg: 'unKnown'
    };
}

3. 高阶函数复用异常处逻辑

const handleTryCatch = (handle: (e: Error) => void = errorHandle) =>
  (fn: (...args: any[]) => Promise<{}>) => async(...args: any[]) => {
    try {
        return [null, await fn(...args)];
    } catch(e) {
        return [handle(e)];
    }
  }

4. 调用

const [error, res] = await handleTryCatch(errorHandle)(fetchFail)(false);
if(error) {
    // 因为 catch 已经做了拦截,甚至可以加入一些通用逻辑,这里甚至不用判断 if error
    console.log(error, 'error');
    return;
}
console.log(res, 'res');

捕获正个页面错误的方法

1. window.onerror

  • 当资源加载失败或无法使用时,会在Window对象触发error事件。例如:script 执行时报错

2. window.onunhandledrejection

  • 当 Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件
  • 这可能发生在 window 下,但也可能发生在 Worker 中

变量类型及拷贝

对象类型及拷贝

对象创建

1. 使用语法结构创建的对象

// 原型链为: o ---> Object.prototype(hasOwnPrototype等方法) ---> null
let o = {a: 1}
// 原型链为: a ---> Array.prototype(包含indexOf、forEach等方法) ---> Object.prototype ---> null
let o = ["yo", "whadup", "?"]
// 原型链为: f ---> Function.prototype(包含call、bind等方法) ---> Object.prototype ---> null
let f = function() {}

2. 使用构造函数创建: 在JS中构造器其实就是一个普通的函数,当使用new操作符来作用函数时,它就可以被称为构造方法。

// g 是生成的对象,他的自身属性有 'vertices' 和 'edges',在 g 被实例化时,g.[[Prototype]] 指向了 Graph.prototype
function Graph() {
    this.vertices = []
    this.edges = []
}  
Graph.prototype = {
    addVertex: function(v){
        this.vertices.push(v)
    }
}  
var g = new Graph()

3. 使用Object.create创建的对象: Object.create(proto, [propertiesObject]),proto为新创建的对象的原型对象

// a ---> Object.prototype ---> null
var a = {a: 1}
// b ---> a ---> Object.prototype ---> null
var b = Object.create(a)
// c ---> b ---> a ---> Object.prototype ---> null
var c = Object.create(b)
// d ---> null
var d = Object.create(null)
// undefined, 因为d没有继承Object.prototype
console.log(d.hasOwnProperty)

4. 使用class关键字创建的对象: ES6引入一套新的关键字来实现class,但JS仍然基于原型,这些新的关键字时class、constructor、static、extends、super

class Person {constructor(name, age) {}}
class Student extends Person {constructor(name, age, salary) {super(name, age)}}

5. 工厂模式: 使用简单的函数创建对象,为对象添加属性和方法,然后返回对象。

6. 原型模式: 使用构造函数的prototype属性来指定哪些应该共享的属性和方法。

7. let clone01 = new HumanCloning() 的实现细节

JS只有函数具备类的概念,因此创建一个对象,必须使用函数对象。函数对象内部有只有用于构造对象的 [[constructor]] 方法和用于函数调用的 [[call]] 方法。var clone01 = new HumanCloning()的实现细节如下:

  • 创建一个build-in object对象并初始化
  • 如果 HumanCloning.prototype 是 object 类型,则将 clone01 的内部 [[prototype]] 设置为 HumanCloning.prototype 否则 为Object.prototype
  • 将 clone01 作为 this,使用 args 参数调用 HumanCloning 的内部 [[call]] 方法
  • 如果 [[call]] 的返回值是 Object 类型,则返回这个值,否则返回 obj

EC/作用域/作用域链

执行上下文(EC)

  • 概念:
    • 执行环境定义了变量或者函数有权访问的其他数据,决定了他们各自的行为
    • 每个执行环境都有一个与之关联的变量对象(VO)
      • 定义的所有变量
      • 声明的函数
    • 虽然我们在编写代码的时候无法访问这个对象,但解析器在处理数据时会在后台用到它
  • 它包含三个部分:
    • 变量对象(VO)
      • 环境中定义的变量
      • 声明的函数
    • 作用域链(词法作用域)
    • this指向
  • 它的类型:
    • 全局环境:是最外围的一个执行环境
      • 在web浏览器中,全局执行环境被认为是window对象
      • 因此所有全局变量和函数都是作为window对象的属性和方法创建的
      • 代码载入浏览器时,全局执行环境被创建。
    • 函数执行环境:
      • 当函数定义时,函数就创建了[Scope]属性
        • 这个属性对应的是一个对象列表
        • 列表中的顺序是上一层函数的Activation Object对象
        • 然后是上上层的
        • 一直到最外层的全局对象。
      • 当函数被调用时,进入函数的执行环境
        • 创建作用域链赋值给一个特殊的内部属性[[scop]]
          • 复制定义时的[[scope]]到作用域链
        • 创建函数的活动对象
          • 使用this,arguments和其它命名参数的值来初始化函数的活动对象
          • (AO = VO + function parameters + arguments)
          • 把活动对象推入scope chain 顶端
        • 当函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。
    • eval()执行环境 - 在eval函数中执行代码也会获得它自己的执行环境。
  • 代码执行过程:
    • 创建全局上下文 (global EC)
    • 全局执行上下文 (caller) 逐行 自上而下 执行。遇到函数时,函数执行上下文 (callee) 被push到执行栈顶层
    • 函数执行上下文被激活,成为 active EC, 开始执行函数中的代码,caller 被挂起
    • 函数执行完后,callee 被pop移除出执行栈,控制权交还全局上下文 (caller),继续执行

作用域

  • 概念:
    • 它定义如何在某些位置存储变量
    • 以及如何在稍后找到这些变量
    • 我们称这组规则为:作用域。
  • 它的类型
    • 全局作用域(变量在函数外定义,变量为全局变量)
    • 局部作用域(变量在函数内声明,变量为局部作用域
      • 局部变量在函数开始执行时创建
      • 函数执行完后局部变量会自动销毁)、
    • 块级作用域(let / const + {})
  • 生命周期
    • JavaScript 变量生命周期在它声明时初始化
    • 局部变量在函数执行完毕后销毁
    • 全局变量在页面关闭后销毁。
  • ES5没有块级作用域
    • 不能根据条件来定义变量
    • 使用var声明的变量会被添加到最接近的环境中
    • 初始化变量时没有使用var声明,该变量会自动被添加到全局变量
var name = "小明";
function getName(){
// var name; 只要函数内定义了一个局部变量,函数在解析的时候都会将这个变量“提前声明”
    alert( name  );    //'undefined'
    var name = '小黄';
    alert(name  );    //小黄
}
getName()

作用域链

  • 作用域链是一个由变量对象组成的带头结点的单向链表,其最前端始终都是当前执行的代码所在环境的变量对象。
  • 用途是保证对执行环境有权访问的所有变量和函数的有序访问。
  • 延长作用域链: try-catch 语句的 catch 块、with 语句
  • 当 JavaScript 代码执行的时候,会进入不同的执行环境,这些执行环境会构成了一个执行环境栈(Execution context stack,ECS)。

闭包

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。在JavaScript中,函数在每次创建时生成闭包。

  • 闭包产生的问题:
    • 多个子函数的[[scope]]都是同时指向父级,是完全共享的。因此当父级的变量对象被修改时,所有子函数都受到影响。
      • 变量可以通过 函数参数的形式 传入,避免使用默认的[[scope]]向上查找
      • 使用setTimeout包裹,通过第三个参数传入
      • 使用 块级作用域,让变量成为自己上下文的属性,避免共享
    • 在闭包中使用this会导致一些问题。this对象是在运行时基于函数的执行环境绑定的: 在全局函数中this等于window,当函数被作为某个对象的方法调用时,this等于那个对象,匿名函数的执行具有全局性,其this通常指向window。
    • 内存泄漏
      • 把闭包引用的对象设置为null。
  • 闭包的使用
    • 闭包可以在js中模仿块级作用域: 创建并立即调用一个函数,这样既可以执行其中的代码,又不会再内存中留下对该函数的引用
    • 闭包可以用于在对象中创建私有变量和特权方法: 构造函数、静态私有变量、模块模式
    • 使用闭包保存变量的当前值,立即执行函数
    • 函数柯里化

函数this指向

this是函数运行时所在的环境对象。 JS函数有四种调用方式,每种方式的不同在于this的初始化。

  1. 函数调用: 非严格模式下this被绑定到全局对象,严格模式下this是undefined。
  2. 方法调用: 函数被赋值给对象的属性后,被称作方法,被调用时this指向调用方法的对象。
  3. 构造函数: 使用new关键字,将function作为构造函数进行调用,this指向被new创建出来的对象。
  4. call和apply: JS中函数也是对象,函数对象也可以包含方法。call()和apply()方法可以用来间接地调用函数。这两个方法允许显示指定调用所需的this的值。call()方法使用它自由的实参列表作为函数的实参,apply()方法则要求以数组的形式传入参数。
  5. function.bind(thisArg[, arg1[, arg2[, ...]]]) 创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为bind()的第一个参数,而其余参数将作为新函数的参数,供调用时使用

call、apply、bind 实现

call、bind 实现源码 bind实现源码

1. call

function.call(thisArg, arg1, arg2, ...) 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数

Function.prototype.myCall = function (object, ...args) {
  object = object || window
  object.fn = this
  let result = object.fn(...args)
  delete object.fn
  return result
}

2. apply

func.apply(thisArg, [argsArray]) 方法调用一个具有给定this值的函数,以及作为一个数组(或类似数组对象)提供的参数。

Function.prototype.myApply = function (object, args) {
  object = object || window
  object.fn = this
  let result
  if (args) {
    result = object.fn(...args)
  } else {
    result = object.fn()
  }
  delete object.fn
  return result
}

3. bind

function.bind(thisArg[, arg1[, arg2[, ...]]]) 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。当使用 new 操作符调用绑定函数时,提供的 this 值被忽略。

Function.prototype.myBind = function(object, ...args) {
    if (typeof this !== "function") {
        throw new Error("Function.prototype.bind - what is trying to be bound is not callable")
    }
    let self = this
    let fBound = function () {
        // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例
        // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 object
        let _this = this instanceof fBound ? this : object
        return self.apply(_this, [...args, ...arguments])
    }    
    // 修改返回函数的 prototype 为绑定函数的 prototype,实例继承绑定函数的原型中的值
    fBound.prototype = Object.create(this.prototype)
    return fBound
}

ES6

babel编译

1. babel编译步骤

  • babylon 将 ES6/ES7 代码解析成 AST
  • babel-traverse 对 AST 进行遍历转译,得到新的 AST
  • 新 AST 通过 babel-generator 转换成 ES5

2. AST

抽象语法树 (Abstract Syntax Tree),是将代码逐字母解析成 树状对象 的形式。这是语言之间的转换、代码语法检查,代码风格检查,代码格式化,代码高亮,代码错误提示,代码自动补全等等的基础。例如:

function square(n){
	return n * n
}

app与 H5 通信

H5 和原生 app(ios,android)交互的载体基本都是基于 Webview,可以把 Webview 看作是一个性能打八折的移动浏览器。

1. 通过 JSBridge

  • 步骤
    • H5->通过某种方式触发一个url
    • Native捕获到url
    • 进行分析
    • 原生做处理
    • Native调用H5的JSBridge对象传递回调
  • 优点:
    • Android4.2以下,addJavascriptInterface方式有安全漏掉
    • iOS7以下,JS无法调用Native
    • url scheme交互方式是一套现有的成熟方案,可以完美兼容各种版本,不存在上述问题

2. 原生方法调用

  • App 调 H5
    • Android 调 H5
      • 4.4之前 loadUrl 无法获取该方法的返回值,函数需在UI线程运行
      • 4.4之后 evaluateJavascript 异步调用JS方法,在 onReceiveValue 中拿到返回值。
    • ios 调 H5
      • 通过 stringByEvaluatingJavaScriptFromString 调用 Html 绑定在 window 上的函数
  • H5 调 App
    • H5 调 Android
      • 通过 addJavascriptInterface 注册,然后 H5 直接调用
    • H5 调 ios
      • 通过 JavaScriptCore 注册,然后 H5 直接调用 3. RN

hybrid app实现了js调用原生模块的功能。本质上H5应用还是运行在webview中,通常一些模块还是基于DOM的,即使是原生模块也是基于代理的方式将js和 native联系在一起。

RN本质上是以React的方式书写代码,然后通过RN这一个中间层,将React转化并调用为原生Native代码

项目性能优化

1. 编码优化

  • js
    • 使用局部变量和扁平化的数据(通过作用域链/原型链读取变量或方法时,需要更多的耗时且越长越慢)(对象嵌套越深,读取值越慢)
    • 避免在循环中执行大量的运算,相同的执行结果应该使用缓存(代码的性能问题会再循环中被指数倍放大)
    • 尽量避免使用 for-in 循环(会枚举原型对象,耗时大于普通循环)
    • 减少访问 dom 的次数(重绘与回流)
    • 使用事件委托
  • css优化
    • 层级扁平
    • 精准样式: 使用 padding-left:10px; 而不是padding: 0 0 0 10px
    • 使用 替代原生 @import
  • html 优化
    • 避免不必要的节点或嵌套,DOM 解析是个大量遍历的过程
    • 避免 src 或 href 等的值为空,即使为空时,浏览器仍然会发起请求
    • 语义化标签,有利于 SEO 与浏览器的解析时间
    • 图片提前指定宽高或者脱离文档流,能有效减少因图片加载导致的页面回流
    • 减少使用 table 进行布局: 避免使用 <br /><hr />
  • 引入外部文件位置
    • css 文件 中引入, js 文件 底部引入
    • 影响首屏的,优先级很高的 js 也可以头部引入,甚至内联

2. 资源缓存

  • http 缓存
    • 不需要缓存(no-store)
    • 频繁变动(no-cache + ETag)
    • 代码文件来说(max-age= + 对文件进行指纹处理,一旦文件名变动就会立刻下载新的文件)
  • 静态文件合理使用 CDN 缓存技术
    • HTML 放于自己的服务器上
    • 打包后的图片 / js / css 等资源上传到 CDN 上,文件带上 hash 值
    • 由于浏览器对单个域名请求的限制,可以将资源放在多个不同域的 CDN 上,可以绕开该限制
  • 服务器缓存
    • 将不变的数据、页面缓存到内存或远程存储(redis等)上
  • 数据缓存:
    • 通过各种存储(storage)将不常变的数据进行缓存,缩短数据的获取时间

3. 资源合并

  • 减少 http 请求
    • 原因
      • http 链接的建立是比较耗时的
      • 每个 http 请求附带额外数据
    • 合并 js,css 文件
      • 公共代码
      • 页面代码
      • 第三方 vender
      • 同域部署减少 options 预检请求
  • 图片优化
    • webp 无损压缩比 PNG 压缩后少 28%
    • css 精灵图
    • base64 图片转换为字符串写入 css 中
    • 合适场景下,使用 iconfont 或者 svg
    • 大量图片不同域名单独部署

4. 减少资源体积

  • gzip 压缩
  • 图片压缩
  • css,js 压缩
  • 删除多余代码
  • 动态 polyfill,只针对不支持的浏览器引入 polyfill

5. 首屏渲染优化

  • css / js 分割,使首屏依赖的文件体积最小,内联首屏关键 css / js
  • 非关键性的文件尽可能的异步加载和懒加载,避免阻塞首页渲染
  • 合理利用 Localstorage / server-worker 等存储方式进行数据与资源缓存
  • 服务端渲染(SSR)
    • 减少首屏需要的数据量,剔除冗余数据和请求
    • 控制好缓存,对数据/页面进行合理的缓存
  • 优化用户感知
    • 尽可能利用骨架屏(Placeholder) / Loading等,减少用户对白屏的感知
    • 利用一些动画过渡效果,能有效减少用户对卡顿的感知
  • 优先渲染一部分
    • 重要的元素优先渲染
    • 视窗内的元素优先渲染