在 JavaScript 中, (a == 1 && a == 2 && a == 3) 是否有可能为 true

参考解决思路:a是一个对象或函数,每次调用取值都不一样,以有序的规律变化就能实现多等

方案1:使用getter

let temp = 1;
Object.defineProperty(global, 'a', {
  get() {
    return temp++;
  }
});
console.log(a === 1 && a === 2 && a === 3); // true

方案2:重写valueOf/toString

const a = {
  value: 1,
  valueOf() {
    return this.value++;
  }
};
console.log(a == 1 && a == 2 && a == 3); // true

从表面看,应该是valueOf()每次都被调用了,但是为什么会这样?我们又没有调用它。这里的valueOf为什么会被调用?这主要是==转换规则

  • 如果一个是null,一个是undefined,则它们相等
  • 如果一个是数字,一个是字符串,先将字符串转换成数字,然后使用转换后的值进行比较
  • 如果其中的一个值为true,则转换成1再进行比较;如果其中一个值为false,这转换成0再进行比较
  • 如果一个值是对象,另一个值是数字或者字符串,则将对象转换成原始值再进行比较。转换成字符串时,会先调用toString(),如果没有toString()方法或者返回的不是一个原始值,则再调用valueOf(),如果还是不存在或者返回不是原始值,则会抛出一个类型错误的异常。返回的原始值会被转换成字符串;如果转换成数字时,也是类似的,不过是会先调用valueOf(),再调用toString(),返回的原始值会被转换成数字
  • 其他不同类型之间的比较均不相等

所以在这里使用a与这些字符进行比较时会被转换成数字,此时会默认调用字符串的valueOf()方法,我们将这个方法进行重写,用于拦截处理a的值

同理可以使用toString方法处理,因为字符串转数字类型时会涉及到valueOf()和toString(),道理一样,只要符合递增规则的,a就可以实现多等,因为此a非彼a

方案3:ES6 Proxy

const a = new Proxy({i: 0}, {
  get: (target, name) => name === Symbol.toPrimitive ? () => ++target.i : target[name]
});
console.log(a == 1 && a == 2 && a == 3);    // true

方案4:数字变量名

var  a = 1;
var ᅠ1 = a;
var ᅠ2 = a;
var ᅠ3 = a;
console.log( a ===ᅠ1 && a ===ᅠ2 && a ===ᅠ3 );

方案5:join+shift

  • 对于对象数组进行比较时,这里数组a每次比较的时候都会默认调用toString(),然后toString()又会默认调用join(),这里将join()改为shift(),意思是删除第一个数组元素值并返回
  • 所以这样调用每次都会导致a数组删除第一个值并且返回删除掉的那个值,结合这样的规律,每次比较都取出对应位置的值
  • 这里是1、2、3,只要符合规律返回的值就行
var a = [1, 2, 3];
a.join = a.shift;
console.log(a);                // [ 1, 2, 3, join: [Function: shift] ]
// console.log(a == 1 && a == 2 && a == 3); // true
console.log(a == 1);        // true
console.log(a);                // [ 2, 3, join: [Function: shift] ]
console.log(a == 2);        // true
console.log(a);                // [ 3, join: [Function: shift] ]
console.log(a == 3);        // true
console.log(a);                // [ join: [Function: shift] ]

js类型的坑

0 == ''
0 == []
0 == ['']

原型链的坑

所有的数组,对象,函数都有一个__proto__,被称为隐式原型。所有的函数都有一个prototype属性,被称为显式原型。prototype是函数的一个属性而已,它和原型没有绝对的关系,每个对象都有一个原型,但是只有函数才会有prototype属性。

var a = function(){};
var b=[1,2,3];

//函数才有prototype属性
console.log(a.prototype);//>> function(){}
//非函数,没有prototype属性
console.log(b.prototype);//>> undefined

每个对象(实例)都有一个属性proto,指向他的构造函数(constructor)的prototype属性,一个对象的原型就是它的构造函数的prototype属性的值。

// 隐式原型等于其构造函数上的显式原型
const obj = {}
obj.__proto__ === Object.prototype
const arr = []
arr.__proto__ === Array.prototype

当一个对象的属性不存在的时候会向其构造函数的显式原型中查找(即该对象本身的隐式原型)。

对象的proto也有自己的proto,层层向上,直到proto为null。换句话说,原型本身也有自己的原型。这种由原型层层链接起来的数据结构成为原型链。因为null不再有原型,所以原型链的末端是null。

使用proto是有争议的,也不鼓励使用它。因为它从来没有被包括在EcmaScript语言规范中,但是现代浏览器都实现了它。proto属性已在ECMAScript 6语言规范中标准化,用于确保Web浏览器的兼容性,因此它未来将被支持。但是,它已被不推荐使用,现在更推荐使用Object.getPrototypeOf/Reflect.getPrototypeOfObject.setPrototypeOf/Reflect.setPrototypeOf(尽管如此,设置对象的原型是一个缓慢的操作,如果性能要求很高,应该避免设置对象的原型)。

使用最新的方法Object.setPrototypeOf(类似Reflect.setPrototypeOf)可以很方便地给对象设置原型,这个对象会继承该原型所有属性和方法。但是,setPrototypeOf的性能很差,我们应该尽量使用 Object.create()来为某个对象设置原型。

//obj的原型是Object.prototype
var obj={
    methodA(){
        console.log("coffe");
    }
}

var newObj = Object.create(obj);//以obj为原型创建一个新的对象

//methodA实际上是newObj原型对象obj上的方法。也即newObj继承了它的原型对象obj的属性和方法。
newObj.methodA();//>> coffe

作用域的坑

fn1() // 不会报错,因为函数会声明前置
function fn1(){}
fn2() // 会报错,var声明前置的时候是undefined
var fn2 = function {}
// 闭包,函数作为参数,函数作为返回值
function F1() {
    var a = 100
    return function () {
        console.log(a) // 自由变量,去父作用域寻找
    }
}

var f1 = F1()

function F2(fn) {
    var a = 200
    fn()
}

F2(f1) // 100

面试题:在页面中创建10个a标签,点击的时候弹出对应的序号,一种常见的错误写法如下:

var i, a;
for (i = 0; i < 10; i++) {
    a = document.createElement('a');
    a.innerHTML = i + '<br>';
    a.addEventListener('click', function (e) {
        e.preventDefault();
        alert(i);
    });
    document.body.appendChild(a);
}

事件监听函数中的i是自由变量,需要去父作用域去寻找,这个时候i已经是10了。

解决方案一:在原来的函数外面包一层函数,将变量作为参数传递进去:

var i, a;
for (i = 0; i < 10; i++) {
    (function (i) {
        a = document.createElement('a');
        a.innerHTML = i + '<br>';
        a.addEventListener('click', function (e) {
            e.preventDefault();
            alert(i);
        });
        document.body.appendChild(a)
    })(i)
}

解决方案二:使用ES6中let的块作用域:

for (let i = 0; i < 10; i++) {
    const a = document.createElement('a');
    a.innerHTML = i + '<br>';
    a.addEventListener('click', function (e) {
        e.preventDefault();
        alert(i);
    });
    document.body.appendChild(a)
}

闭包在实际开发中重要用于封装变量,收敛权限

跨域

可以跨域的3个标签:

  • img:可以用于打点统计,统计网站可能是其他域,使用img标签有一个好处:没有浏览器兼容性问题
  • link:可以使用CDN
  • script:可以使用CDN,可以JSONP

本地3000端口开启了一个服务:

const http = require('http')

http.createServer((req, res) => {
    res.end(JSON.stringify({
        now: new Date().toString()
    }))
}).listen(3000)
console.log('api server started at 3000')

编写页面如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>跨域</title>
</head>

<body>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script>
        axios.get('http://127.0.0.1:3000')
            .then(function (response) {
                console.log(response);
            })
            .catch(function (error) {
                console.log(error);
            });
    </script>
</body>

</html>
image.png
image.png

使用JSONP实现跨域,服务端和客户端都进行一个很小的改造:

const http = require('http')

http.createServer((req, res) => {
    const data = `callback(${JSON.stringify({ now: new Date().toString() })})`
    res.end(data)
}).listen(3000)
console.log('api server started at 3000')

客户端:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>跨域</title>
</head>

<body>
    <script>
        function callback(data) {
            console.log('get data', data)
        }
    </script>
    <script src="http://127.0.0.1:3000"></script>
</body>

</html>
image.png
image.png

以上是原理部分,就是服务器返回的数据包了一层函数,客户端有个同样名字的函数去接收。实际的开发中有很多现成的库。

为什么js要放在body最下面二css要放在head中?

浏览器拿到css就知道该怎么渲染HTML了,如果把css放在body下面则会先按照默认样式进行渲染,当css加载完成的时候会按照css的样式再渲染一遍,即发生了reflow。由于js会阻塞代码执行,所以js一般放在body最下面,还有一个原因:js放在最下面的时候就可以取得页面中所有的元素了。

window.onload和DomContentLoaded有什么区别

前者是页面的资源加载完成(包括图片、视频),后者是DOM渲染完即可,此时图片、视频可能还没加载完。

图片懒加载的实现原理

先用一张默认的图片代替(这张默认的图片因为使用太频繁,浏览器可能缓存了)然后再更改img的src属性。

image.png
image.png

实现new运算符

function myNew() {
  const constr = Array.prototype.shift.call(arguments) // 第一个参数是构造函数
  const obj = Object.create(constr.prototype) // 创新一个新对象,该对象的原型是constr
  const result = constr.apply(obj, arguments) // 改变this的指向,让this指向刚才传创建的obj
  return result instanceof Object ? result : obj // 判断返回值是不是对象,如果是返回,如果不是返回刚才创建的新对象
}

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function () {
  console.log(`I am ${this.name},is ${this.age} years old!`)
}

const p = myNew(Person, '张三', 18)
console.log(p) // Person { name: '张三', age: 18 }
p.sayHello() // I am 张三,is 18 years old!

实现call

思路:

  1. 参考call的语法规则,需要设置一个参数thisArg,也就是this的指向;
  2. 将thisArg封装为一个Object;
  3. 通过为thisArg创建一个临时方法,这样thisArg就是调用该临时方法的对象了,会将该临时方法的this隐式指向到thisArg上
  4. 执行thisArg的临时方法,并传递参数;
  5. 删除临时方法,返回方法的执行结果。
/**
 * 用原生JavaScript实现call
 */
Function.prototype.myCall = function(thisArg, ...arr) {

  //1.判断参数合法性/////////////////////////
  if (thisArg === null || thisArg === undefined) {
    //指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
    thisArg = window;
  } else {
    // 当以非构造函数形式被调用时,Object 等同于 new Object()。
    thisArg = Object(thisArg);//创建一个可包含数字/字符串/布尔值的对象,
                              //thisArg 会指向一个包含该原始值的对象。
  }

  //2.搞定this的指向/////////////////////////
  const specialMethod = Symbol("anything"); //创建一个不重复的常量
  //如果调用myCall的函数名是func,也即以func.myCall()形式调用;
  //根据上篇文章介绍,则myCall函数体内的this指向func
  thisArg[specialMethod] = this; //给thisArg对象建一个临时属性来储存this(也即func函数)
  //进一步地,根据上篇文章介绍,func作为thisArg对象的一个方法被调用,那么func中的this便
  //指向thisArg对象。由此,巧妙地完成将this隐式地指向到thisArg!
  let result = thisArg[specialMethod](...arr);

  //3.收尾
  delete thisArg[specialMethod]; //删除临时方法
  return result; //返回临时方法的执行结果
};

let obj = {
  name: "coffe1891"
};

function func() {
  console.log(this.name);
}

func.myCall(obj);//>> coffe1891

实现apply

  1. 传递给函数的参数处理,不太一样,其他部分跟call一样;
  2. apply接受第二个参数为类数组对象, 这里用了《JavaScript权威指南》一书中判断是否为类数组对象的方法。
/**
 * 用原生JavaScript实现apply
 */
Function.prototype.myApply = function(thisArg) {
  if (thisArg === null || thisArg === undefined) {
    thisArg = window;
  } else {
    thisArg = Object(thisArg);
  }

  //判断是否为【类数组对象】
  function isArrayLike(o) {
    if (
      o && // o不是null、undefined等
      typeof o === "object" && // o是对象
      isFinite(o.length) && // o.length是有限数值
      o.length >= 0 && // o.length为非负值
      o.length === Math.floor(o.length) && // o.length是整数
      o.length < 4294967296
    )
      // o.length < 2^32
      return true;
    else return false;
  }

  const specialMethod = Symbol("anything");
  thisArg[specialMethod] = this;

  let args = arguments[1]; // 获取参数数组
  let result;

  // 处理传进来的第二个参数
  if (args) {
    // 是否传递第二个参数
    if (!Array.isArray(args) && !isArrayLike(args)) {
      throw new TypeError(
        "第二个参数既不为数组,也不为类数组对象。抛出错误"
      );
    } else {
      args = Array.from(args); // 转为数组
      result = thisArg[specialMethod](...args); // 执行函数并展开数组,传递函数参数
    }
  } else {
    result = thisArg[specialMethod]();
  }

  delete thisArg[specialMethod];
  return result; // 返回函数执行结果
};

实现bind

 * 用原生JavaScript实现bind
 */
Function.prototype.myBind = function(objThis, ...params) {
  const thisFn = this;//存储调用函数,以及上方的params(函数参数)
  //对返回的函数 secondParams 二次传参
  let funcForBind = function(...secondParams) {
    //检查this是否是funcForBind的实例?也就是检查funcForBind是否通过new调用
    const isNew = this instanceof funcForBind;

    //new调用就绑定到this上,否则就绑定到传入的objThis上
    const thisArg = isNew ? this : Object(objThis);

    //用call执行调用函数,绑定this的指向,并传递参数。返回执行结果
    return thisFn.call(thisArg, ...params, ...secondParams);
  };

  //复制调用函数的prototype给funcForBind
  funcForBind.prototype = Object.create(thisFn.prototype);
  return funcForBind;//返回拷贝的函数
};
let func = function(p,secondParams){//其实测试用的func其参数可以是任意多个
    console.log(p.name);
    console.log(this.name);
    console.log(secondParams);
}
let obj={
    name:"1891"
}
func.myBind(obj,{name:"coffe"})("二次传参");
//>> coffe
//>> 1891
//>> 二次传参

实现函数重载

重载是面向对象编程语言(比如Java、C#)里的特性,JavaScript语言并不支持该特性。所谓重载(overload),就是函数名称一样,但是随着传入的参数个数不一样,调用的逻辑或返回的结果会不一样。jQuery之父John Resig曾经提供了一个非常巧妙的思路实现重载,代码如下:

(() => {//IIFE+箭头函数,把要写的代码包起来,避免影响外界,这是个好习惯

  // 当函数成为对象的一个属性的时候,可以称之为该对象的方法。

  /**
   * @param {object}  一个对象,以便接下来给这个对象添加重载的函数(方法)
   * @param {name}    object被重载的函数(方法)名
   * @param {fn}      被添加进object参与重载的函数逻辑
   */
  function overload(object, name, fn) {
    var oldMethod = object[name];//存放旧函数,本办法灵魂所在,将多个fn串联起来
    object[name] = function () {
      // fn.length为fn定义时的参数个数,arguments.length为重载方法被"调用"时的参数个数
      if (fn.length === arguments.length) {//若参数个数匹配上
        return fn.apply(this, arguments);//就调用指定的函数fn
      } else if (typeof oldMethod === "function") {//若参数个数不匹配
        return oldMethod.apply(this, arguments);//就调旧函数
                                                //注意:当多次调用overload()时,旧函数中
                                                //又有旧函数,层层嵌套,递归地执行if..else
                                                //判断,直到找到参数个数匹配的fn
      }
    };
  }

  // 不传参数时
  function fn0() {
    return "no param";
  }

  // 传1个参数
  function fn1(param1) {
    return "1 param:" + param1;
  }

  // 传两个参数时,返回param1和param2都匹配的name
  function fn2(param1, param2) {
    return "2 param:" + [param1, param2];
  }

  let obj = {};//定义一个对象,以便接下来给它的方法进行重载

  overload(obj, "fn", fn0);//给obj添加第1个重载的函数
  overload(obj, "fn", fn1);//给obj添加第2个重载的函数
  overload(obj, "fn", fn2);//给obj添加第3个重载的函数

  console.log(obj.fn());//>> no param
  console.log(obj.fn(1));//>> 1 param:1
  console.log(obj.fn(1, 2));//>> 2 param:1,2
})();

Promise相关

怎样控制Promise的并发

我们知道Promise.all可以让Promise并发执行,但是这些Promise是创建的时候就开始执行了,所有数组中的Promise可以理解为同时执行,如何限制并发执行的个数呢?只能从Promise创建的时候开始考虑了。维护一个正在执行的Promise的队列记为executing,并发数记为poolLimit,当正在执行的Promise数量大于等于poolLimit就需要等待executing中一个Promise执行完腾出位置了,Promise.race提供了这样一组api:接收一个Promise数组,当第一个Promise执行完的时候,整个Promise.race执行结束。我们正好可以利用这个特性,及时腾出位置加入其它的Promise到executing中!

function asyncPool(poolLimit, array, iteratorFn) {
  let i = 0;
  const ret = [];
  const executing = new Set();
  const enqueue = function () {
    // 边界处理,array为空数组
    if (i === array.length) {
      return Promise.resolve();
    }
    // 每调一次enqueue,初始化一个promise(使用Promise.resolve.then包装一下,这样返回的都是promise了)
    const item = array[i++];
    const p = Promise.resolve().then(() => iteratorFn(item, array));
    // 放入promises数组
    ret.push(p);
    executing.add(p);
    // promise执行完成后,从执行队列中删除
    p.then(() => {
      executing.delete(p);
    });

    // 使用Promise.race,每当executing队列中promise数量低于poolLimit,就实例化新的promise并执行
    let r = Promise.resolve();
    if (executing.size >= poolLimit) {
      r = Promise.race(executing);
    }
    // 递归,直到遍历完array
    return r.then(() => enqueue());
  };
  return enqueue().then(() => Promise.all(ret));
}
const timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i));
require('util').log('start');
asyncPool(2, [1000, 5000, 3000, 2000], timeout).then(results => {
  require('util').log(results);
});
// 8 Sep 17:20:07 - start
// 8 Sep 17:20:13 - [ 1000, 5000, 3000, 2000 ]

其实所谓的Promise并发控制,就是控制Promise实例化的个数。然而这样的实现效果本质上来说已经摈弃了Promise.all,期待标准库中可以提供这个功能。

如何取消Promise

promise一旦创建是不能取消的,我们只能对其进行包装:

const makeCancelable = (promise) => {
  let hasCanceled_ = false;
  const wrappedPromise = new Promise((resolve, reject) => {
    promise.then((val) =>
      hasCanceled_ ? reject({isCanceled: true}) : resolve(val)
    );
    promise.catch((error) =>
      hasCanceled_ ? reject({isCanceled: true}) : reject(error)
    );
  });
  return {
    promise: wrappedPromise,
    cancel() {
      hasCanceled_ = true;
    },
  };
};

const somePromise = new Promise(r => setTimeout(r, 1000));//创建一个异步操作
const cancelable = makeCancelable(somePromise);//为异步操作添加可取消的功能
cancelable
  .promise
  .then(() => console.log('resolved'))
  .catch(({isCanceled, ...error}) => console.log('isCanceled', isCanceled));
// 取消异步操作
cancelable.cancel();

出处:https://github.com/crazycodeboy/RNStudyNotes/tree/master/React%20Native%20%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/React%20Native%20%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E4%B9%8B%E5%8F%AF%E5%8F%96%E6%B6%88%E7%9A%84%E5%BC%82%E6%AD%A5%E6%93%8D%E4%BD%9C

node.js源码

nodejs中的global对象什么时候初始化

global对象在src/node.cc中的被创建,在bootstrap/node.js中被初始化。在src/node.cc的LoadEnvironment方法中,有以下几行代码是用来创建global对象的。

// Add a reference to the global object
Local<Object> global = env->context()->Global();

// Expose the global object as a property on itself
// (Allows you to set stuff on `global` from anywhere in JavaScript.)
global->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "global"), global);

其中env->context()->Global()获取了当前context中的Global全局对象,global->Set(FIXED_ONE_BYTE_STRING(env->isolate(), “global”), global)将全局对象本身挂载在其global对象上,这样全局对象中有了一个global对象指向了全局对象本身,我们在该context中可以直接使用global对全局对象的引用进行访问。以上程序之后,这样我们就可以在context的任何地方对全局对象进行操作。

初始化在bootstrap/node.js中的setupGlobalVariables进行,其在上述逻辑执行后被执行,所以可以直接操作全局对象。这个函数中将process、Buffer直接放在global对象上,因此我们可以直接访问这些全局对象了。进一步地,setTimeout等定时器通过setupGlobalTimeouts方法放在global上。

事件循环

参考国外大神写的一系列文章