标签归档:JavaScript

EventSource

EventSource 是浏览器端配合服务端事件(Server-Sent Events)使用的技术。和 WebSocket 相比,服务端事件是单向推送,而 WebSocket 是双向通信。

浏览器端

// 第二个参数是可选的,withCredentials 目前用于跨域 cookie,默认 false
const es = new EventSource(url, { withCredentials: true });
es.addEventListener('error', err => console.error(err));
// 监听 message 事件
es.addEventListener('message', evt => console.log(evt.data));
// 监听 type/custom 事件
es.addEventListener('type/custom', evt => console.log(evt.data));

EventSource 要求的是持续不断的输出,如果请求正常地结束终止了,那么客户端会再次自动发起连接,除非显式地调用 close() 方法来结束连接。

服务端

服务端的实现是返回标记为 Content-Type: text/event-stream 的持续内容,其内容的基本格式是:

event: evt-type
data: any message

其中,event 对应 EventSource 监听的事件类型,但该内容是可选的,如果没指定 event,那么则被 message 事件监听(可简化为 es.onmessage)。

紧邻的 data 字段会把内容合并起来,连接内容是一个空行。

直接以 : 开头的内容则是注释,不被解析使用。

继续阅读

Proxy in JavaScript

可以代理一个对象,对读取、赋值等操作做拦截,从而可额外地做一些事情,对于没有拦截的操作,则按原对象操作调用。

let p = new Proxy(target, handler);

通过 handler 来提供以下方法(this 指向 代理后的对象,该对象的 [[handler]] 指向 handler):

  • get(target, prop, receiver) 拦截属性读取操作
  • set(target, prop, val, receiver) 拦截设置属性值操作
  • has(target, prop) 可以看作是对 in 的操作的拦截
  • apply(target, thisArg, args) 拦截函数的调用,包括普通调用和 applycall 等。
  • construct(target, args, newTarget) 拦截 new 操作符
  • defineproperty(target, prop, descriptor) 拦截 Object.defineProperty 的操作
  • deleteProperty(target, prop) 拦截 delete 操作
  • getOwnPropertyDescriptor(target, prop) 拦截 Object.getOwnpropertyDescriptor
  • getPrototypeOf(target) 拦截原型对象的读取,包括 Object.getPrototypeOf()__proto__ 以及 instanceof
  • isExtensible(target) 拦截 Object.isExtensible()
  • ownKeys(target) 拦截 Object.getOwnPropertyNames() 以及 Object.keys()
  • preventExtensions(target) 拦截 Object.preventExtensions(obj)
  • setPrototypeOf(target, proto) 拦截 Object.setPrototypeOf

receiver,Proxy 或继承 Proxy 的对象,最初被调用的对象,通常就是 proxy 自身。但如果对象本身无该属性,原型链上有 Proxy 对象,那么这时对象本身就作为 receiver 参数传进来了。1

不仅使用于 Object,也使用于 Reflect,拦截具体对应的操作可能有所区别

Proxy.revocable(target, handler) 创建一个 proxy 并返回撤销该 proxy 的方法。

const { proxy, revoke } = Proxy.revocable(target, handler);
// .. do something with proxy
// 撤销代理
revoke();

参考

JS 模块多次 require

Node.JS 中,同一个模块多次 require 得到的是同一个对象,这意味着修改了一个模块的内部值是可以在别的引用了该模块的地方生效的。

Webpack 的打包机制也是基于引用同一个对象来实现的,故同理。

示例代码

mod.js

module.exports = test;

function test() {
   return test.data; 
}

test.add = function(data) {
    this.data.push(data);
};

test.data = [];

comA.js

const mod = require('./mod');

mod.add(1);
mod.add(2);
mod.add(3);

console.log('in comA');

index.js

const mod = require('./mod');
require('./comA');

console.log(mod());   // ==> [1, 2, 3]

Arrow function 与 bind

JavaScript 中 this 的指向有一些规则,其中可通过 bindarrow function 来绑定 this 的指向。

arrow function 在 ES6 中引入,由于可以绑定 this 的原因,常用来替换普通的匿名函数(function() {})。

这两种方式在基于 React 技术中较为广泛地使用,主要是用于事件绑定中。虽然同可绑定 this,但配合类使用时实际是有差别的。简单来说,基于 arrow function 声明的类属性将无法被继承调用。

class A {
    constructor() {
        this.bindMethod = this.bindMethod.bind(this);
    }

    m(p) {
        console.log('m', p);
    }

    bindMethod(p) {
        console.log('bindMethod', p);
    }

    arr = p => {
        console.log('arrow', p);
    };
}

class B extends A {
    m(p) {
        console.log('B');
        super.m(p);
    }

    bindMethod(p) {
        console.log('B');
        super.bindMethod(p);
    }

    arr = p => {
        console.log('B');
        super.arr(p); // Error run with compiled code with es2015+stage0
    };
}

const a = new A();
a.m(1);
a.bindMethod(2);
a.arr(3);

const b = new B();
b.m(1);
b.bindMethod(2);
b.arr(3);

babel 结果(ES2017 + Stage 0)

上面的 class A babel 后(把目标设为 ES2017 是为了简化掉实现 class 继承机制的代码),其中,arrow 形式的 class 属性变成了实例属性,而非原型属性,这样就子类将无法通过 super 来调用。

而通过 bind 方式来实现的 this 绑定则依然保留有原型方法。

class A {
    constructor() {
        this.arr = p => {
            console.log("arrow", p);
        };

        this.bindMethod = this.bindMethod.bind(this);
    }

    m(p) {
        console.log("m", p);
    }

    bindMethod(p) {
        console.log("bindMethod", p);
    }
}

参考

JavaScript this 值

JavaScript 中 this 的指向根据上下文及使用方式而有所不同。

全局上下文 Global Context

在全局上下文(不在函数体内)中总指向全局对象,如在浏览器中是 window,在 Node.js 则是 global

函数上下文 Function Context

  • 简单调用
    • 严格模式:undefined
    • 非严格模式:全局对象
    • 可用 callapply 来改变 this
  • 作为对象的方法:指向直接所在的对象,如 a.b.c()cthis 指向 b,适用于普通的对象、原型链方法和 getter/setter
  • 作为构造函数,指向正在构建的对象
  • 作为 DOM 事件处理函数,指向触发事件的元素(指的是外层函数,如果是多层,遵循其余规则)
  • bind:用 f.bind(object) 来返回新的绑定 this 的函数,不论怎么调用,如通过对象方法调用、callapply 都不会改变 this。再次 bind 也不会改变。
  • 箭头函数(arrow function):this 指向词法上下文(实际运行生成箭头函数时上下文)的 this,不会被调用方式改变,但会因生成方式而不同。
var obj = {
    bar: function() {
        var x = (() => this);
        return x;
    }
};

var fn = obj.bar();
console.log(fn() === obj); // true

var fn2 = obj.bar;
console.log(fn2()() == window); // true

继续阅读

import 与 require 的两个差异

Javascript 的模块化开发,除了 RequireJS 代表的 AMD 外,目前广泛使用的是 Node.js 代表的 CommonJS 以及 ES6 的 module。

在 babel / webpack 等工具链下,大多数情况下,CommonJS 的 require 和 ES6 的 import 效果等同,babel 的 preset 是 ES2015 以及 webpack 都是把 import 转为 require(webpack 根据 target 再转为 AMD 或 UMP 等格式)。

但两个规范实质上并不等同,所以存在着不少的区别,如不能 import(var) 但能 require(var),详细可查看 ES6 模块系统,此处记录一下两个可能比较直接影响代码书写的差异点。

继续阅读

浏览器端 async/await 还是有点代价的

对于下面这段 js

function async3() {
  createPromise()
    .then((res) => {
        console.log(res);
    })
    .catch((err) => {
        console.log(err);
    })
    .finally(() => {
        console.log('finally');
    });
}

其对应 async/await 版本是

async function async2() {
    try {
      const ret = await createPromise();
      console.log(ret);
    } catch (e) {
      console.error(e);
    } finally {
      console.log('finally');
    }
}

经 babel 转为 ES5 后,除了必要的 babel-polypill,会生成以下代码,其中 _asyncToGenerator 不会多次生成。

var async2 = function () {
  var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
    var ret;
    return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            _context.prev = 0;
            _context.next = 3;
            return createPromise();

          case 3:
            ret = _context.sent;

            console.log(ret);
            _context.next = 10;
            break;

          case 7:
            _context.prev = 7;
            _context.t0 = _context['catch'](0);

            console.error(_context.t0);

          case 10:
            _context.prev = 10;

            console.log('finally');
            return _context.finish(10);

          case 13:
          case 'end':
            return _context.stop();
        }
      }
    }, _callee, this, [[0, 7, 10, 13]]);
  }));

  return function async2() {
    return _ref.apply(this, arguments);
  };
}();

function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }

写法上直观了的,但在浏览器端,还是有些代价,生成的代码会多出不少。

fetch

实现中的 fetch 是希望能比 XMLHttpRequest 更好地做网络请求。

fetch('/')
    .then(res => { 
       if(res.ok) {
         return res.text();
       } else {
         throw new TypeError(res.statusText);
       }
    })
    .then(text => console.log(text.length))
    .catch(err => console.error(err.message));

Reference

注意点:

  1. fetch 仅在网络因素下 reject,业务的错误,如 502 或 404 等会 resolve,需要判断 response 是否 ok
  2. 默认不携带 cookie,需要明确指定
  3. 跨域时会受 CSP 控制影响,需要配置

JavaScript 中的零除

一直意味在 JavaScript 中也和别的语言一样会触发错误,所以一直依赖使用除法都先小心地做个判断。但偶然中却发现并不需要在做除法前判断,因为不会报错,只需要拿到结果后进行处理即可。

发现不会报错后,稍微去看了一下规定:

Infinity / Infinity = NaN
Infinity / 0 = Infinity
-Infinity / 0 = -Infinity
Infinity / nonZeroFinite = Infinity
-Infinity / nonZeroFinite = -Infinity
aPositive / Infinity = 0
aNegative / Infinity = -0
0 / 0 = NaN
aPositive / 0 = Infinity
aNegative / 0 - -Infinity