判断变量类型的几个方法

前置

function Person() {}
const p = new Person()

一、 typeof

console.log(typeof []) // object
console.log(typeof {}) // object
console.log(typeof function(){}) // function
console.log(typeof '') // string
console.log(typeof 1) // number
console.log(typeof true) // boolean
console.log(typeof undefined) // undefined
console.log(typeof null) // object
console.log(typeof NaN) // number
console.log(typeof /abc/) // object
console.log(typeof Symbol()) // symbol
console.log(typeof new Date()) // object
console.log(typeof new Map()) // object
console.log(typeof new Set()) // object
console.log(typeof Person) // function
console.log(typeof p) // object

区分不了更精细的对象,比如数组和后面三个也被判断为对象。虽说数组和函数也是对象的一种。但函数都有自己的姓名。
参考了 w3help 上的资料来看,标准如此:

最新的 标准 图是这样子:

PS:
__native object:__原生对象,ECMAScript 实现中,并非由宿主环境,而是完全由本规范定义其语义的对象。这些引用类型在运行过程中需要通过new来创建所需的实例对象。如:Object、Array、Date、RegExp、Function、Boolean、Number、String。(听起来就是语言标准自带的)
__built-in object:__内置对象,由 ECMAScript 实现提供,独立于宿主环境的对象,ECMAScript 程序开始执行时就存在。内置对象是本地对象的子集。包含:Global和Math、JSON。
__host object:__宿主对象,由宿主环境提供的对象,用于完善 ECMAScript 执行环境,如 Window 和 Document。

看起来数组属于 native object,同时没有 implement [[Call]] (没有这个内置属性?),所以返回会是个对象。

不过如果不需要判断得那么精细,那也够用了。而且这个可以直接 typeof 一个没有定义过的变量,会返回undefined。或许有些地方也可以利用起来。
另,可以看下 segmentfault 大佬对于 typeof(null) === 'object' 的回答。

二、 constructor

console.log([].constructor.name) // Array
console.log({}.constructor.name) // Object
console.log(function(){}.constructor.name) // Function
console.log(''.constructor.name) // String
const number = 1
console.log(number.constructor.name) // Number
console.log(true.constructor.name) // Boolean
// console.log(undefined.constructor.name) // 报错
// console.log(null.constructor.name) // 报错
console.log(NaN.constructor.name) // Number
console.log(/abc/.constructor.name) // RegExp
console.log(Symbol().constructor.name) // Symbol
console.log(new Date().constructor.name) // Date
console.log(new Map().constructor.name) // Map
console.log(new Set().constructor.name) // Set
console.log(Person.constructor.name) // Function
console.log(p.constructor.name) // Person

只有 constructor 也行,返回就是这个这个变量的原型。

需要注意的是实例 p 返回的是 Person 这个原型。

三、 instanceof

先看 定义。从名字上看,该变量是否为某原型的实例。

console.log([] instanceof Array) // true
console.log([] instanceof Object) // true
console.log({} instanceof Object) // true
console.log(function(){} instanceof Function) // true
console.log('' instanceof String) // false
console.log(1 instanceof Number) // false
console.log(true instanceof Boolean) // false
// console.log(undefined instanceof undefined)
// 报错:Right-hand side of 'instanceof' is not an object
// console.log(null instanceof null)
// 报错:Right-hand side of 'instanceof' is not an object
console.log(NaN instanceof Number) // false
console.log(/abc/ instanceof RegExp) // true
console.log(Symbol() instanceof Symbol) // false
console.log(new Date() instanceof Date) // true
console.log(new Map() instanceof Map) // true
console.log(new Set() instanceof Set) // true
console.log(Person instanceof Function) // true
console.log(p instanceof Person) // true
console.log(p instanceof Object) // true

[]既是数组的实例,又是对象的实例。

可参阅此文章介绍,这里直接摘取下里面的翻译与代码

function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
  var O = R.prototype;// 取 R 的显示原型
  L = L.__proto__;// 取 L 的隐式原型
  while (true) {
    if (L === null)
      return false;
    if (O === L)// 这里重点:当 O 严格等于 L 时,返回 true
      return true;
    L = L.__proto__;
  }
}
// 上面的代码就很好地解释了,根据原型链的知识,我们知道以下两条等式成立,所以 [] 也是 Object 的实例
[].__proto__ === Array.prototype
Array.prototype.__proto__ === Object.prototype

这时候又出现了个问题,NaN instanceof Number === false…真是没完没了。按照上面的方法来应该是true的。
其实这个可以跟上面的 '' instanceof String === false 1 instanceof Number === false true instanceof Boolean === false 一起看。
NaN也是一个数字值,跟1、2、3这种一样。上面那样写就是返回 false,写出 new 出来的实例就为 true。比如:

const num = new Number(1)
console.log(num instanceof Number) // true

个人理解:这里的 num 是 Number 的一个实例,但其实它是对象。实例首先肯定是个对象。比如 1 ‘’ true,都是基础类型。上面的方法是站在真正的实例对象来判断,是一个简单化步骤的方法。
但标准里面说的,还有调用内部方法与内部属性,当判断到不是对象,其实就返回 false 了。这里与 typeof 联系起来,typeof 不是 object 的,用 instanceof 都是 false 了

四、 Object.prototype.toString.call

console.log(Object.prototype.toString.call([])) // [object Array]
console.log(Object.prototype.toString.call({})) // [object Object]
console.log(Object.prototype.toString.call(function(){})) // [object Function]
console.log(Object.prototype.toString.call('')) // [object String]
console.log(Object.prototype.toString.call(1)) // [object Number]
console.log(Object.prototype.toString.call(true)) // [object Boolean]
console.log(Object.prototype.toString.call(undefined)) // [object Undefined]
console.log(Object.prototype.toString.call(null)) // [object Null]
console.log(Object.prototype.toString.call(NaN)) // [object Number]
console.log(Object.prototype.toString.call(/abc/)) // [object RegExp]
console.log(Object.prototype.toString.call(Symbol())) // [object Symbol]
console.log(Object.prototype.toString.call(new Date())) // [object Date]
console.log(Object.prototype.toString.call(new Map())) // [object Map]
console.log(Object.prototype.toString.call(new Set())) // [object Set]
console.log(Object.prototype.toString.call(Person)) // [object Function]
console.log(Object.prototype.toString.call(p)) // [object Object]
console.log([1,2,3,4].toString()) // 1,2,3,4

这个就厉害了,居然精确到每一个都不一样。细看跟 typeof 还有点像,但是精细很多。所以一定也是有个什么内置属性在记录着这个东西。
要写成 Object.prototype.toString.call 这个样子,是因为别的原型重写了该方法,比如最后一个数组的 toString 方法返回就不一样了。
在控制台打印一个数组看看,就能清晰看到 Array 本身也有一个 toString 方法。

简而言之,分两种,一种是对象内置了 Symbol.toStringTag 这个属性,来返回 [object ${tag}] tag 部分的值。
另外一种是没有这个内置属性,但是 语言标准tc39 为其指定了返回 tag,比如:Array String 等这几个老面孔。
这也就意味着可以自定义自己的tag。


console.log(Promise.prototype[Symbol.toStringTag]) // Promise
console.log(Array.prototype[Symbol.toStringTag]) // undefined

function Tag() {}
Tag.prototype[Symbol.toStringTag] = 'newTag'
const t = new Tag()
console.log(Object.prototype.toString.call(t)) // [object newTag]

function Tag1() {}
Tag1.prototype[Symbol.toStringTag] = {}
const t1 = new Tag1()
console.log(Object.prototype.toString.call(t1)) // [object Object]
// 从上面 tc39 的介绍14,15,16来看,当 [Symbol.toStringTag] 不为 string,则将内置 tag 设置为 Object

里面还涉及其他知识,可以参阅 知乎MDN-Symbol.toStringTagMDN-toString

码后感:几个判断类型方法,居然有这么多细节点在里面。平时可能用了就用了,都不知道其中原理。真是路漫漫啊。。。

遍历属性的几个方法

这里列出五个

  • for in
  • Object.keys
  • Object.getOwnPropertyNames
  • Object.getOwnPropertySymbols
  • Reflect.ownKeys
Object.prototype.allattr = true
Object.prototype.allFn = function(){}

// 普通对象
const obj1 = {
  a: 1,
  b: 2,
  [Symbol('c')]: 3
}
Object.defineProperty(obj1, 'd', {
  enumerable: false,
  value: 4
})

// 原型对象
function Obj2 () {
  this.a = 1
  this[Symbol('b')] = 1
}
Obj2.prototype.c = 2
Obj2.prototype.d = function() {}
Obj2.e = 1

// 对象实例
const obj3 = new Obj2()

function show(obj) {
  let arr = []
  // 会打出原型的属性,可以用 hasOwnProperty 去除
  for (const attr in obj) {
    arr.push(attr)
  }
  console.log('--- for in ---', arr)

  arr = []
  // 不会打出原型的属性,相当于 for in 用 hasOwnProperty 去除
  Object.keys(obj).forEach((attr) => {
    arr.push(attr)
  })
  console.log('--- Object.keys ---', arr)

  arr = []
  // 上面两个只打出可枚举的,这个不可枚举也可以打出来,不会打出原型的属性
  Object.getOwnPropertyNames(obj).forEach((attr) => {
    arr.push(attr)
  })
  console.log('--- Object.getOwnPropertyNames ---', arr)

  arr = []
  // 打出 Symbols 属性
  Object.getOwnPropertySymbols(obj).forEach((attr) => {
    arr.push(attr)
  })
  console.log('--- Object.getOwnPropertySymbols ---', arr)

  arr = []
  // Reflect.ownKeys = Object.getOwnPropertyNames + Object.getOwnPropertySymbols
  Reflect.ownKeys(obj).forEach((attr) => {
    arr.push(attr)
  })
  console.log('--- Reflect.ownKeys ---', arr)
}

show(obj1)
console.log('%c --------------- 分割线 ---------------', 'background:#aaa;color:red')
show(Obj2)
console.log('%c --------------- 分割线 ---------------', 'background:#aaa;color:red')
show(obj3)

简单实现图片懒加载、预加载、分批加载

图片我这里是随便找了十张图片,起名 0~9 的名字,方便获取。

懒加载:原理就是先放一批空图片占位,当图片进入可视区,再来赋予图片src值,让其显示。
预加载:提前请求一批图片资源。下载缓存里。到真的渲染图片时候就可以快速获取到图片。用 new Image() 来请求资源,就不用先创建dom。
分批加载:设定一个距离值,在滑动到距离底部小于这个距离值,则请求下一批图片。

分批加载可以与其他两者混用,懒加载与预加载应该不混用了,一个减低服务器压力,一个是增加服务器压力换取体验。

<!DOCTYPE>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>lazyload</title>
</head>
<style type="text/css">
  #box {
    width: 100%;
    background: black;
    border-radius: 10px;
  }
  ul {
    padding: 0;
    margin: 0;
  }
  li {
    margin: 5px;
    background: gray;
    text-align: center;
  }
  img {
    height: 300px;
    width: auto;
  }
</style>
<body>
<script type="text/javascript">
// 简单节流
function throttle(fn, time) {
  let timer
  return (ev) => {
    if (timer) {
      return
    }
    timer = setTimeout(() => {
      timer = null
      fn(ev)
    }, time)
  }
}
function getNum(numMin, numMax) {
  return Math.round( Math.random()*(numMax-numMin) + numMin );
}
function getImg (len) {
  const arr = []
  for (let i = 0; i < len; i++) {
    arr.push(getNum(0, 9))
  }
  // 模拟异步获取图片资源
  return Promise.resolve(arr)
}
window.onload = function () {
  const $ui = document.querySelector('#box ul')
  const loading = './img/loading.gif'
  const distance = 20
  function initImg() {
    return getImg(5).then((imgs) =>{
      imgs.forEach((img) => {
        const url = `./img/${img}.JPG`
        // 先预加载,演示一下,不然这里懒加载没什么意义了,都是发了请求
        // 用一个img对象来先对资源进行请求
        const imgObj = new Image()
        imgObj.src = url
        // 创建图片dom
        const $li = document.createElement('li')
        $ui.appendChild($li)
        const $img = document.createElement('img')
        $img.src = loading
        $img.dataset.src = `./img/${img}.JPG`
        $li.appendChild($img)
      })
    })    
  }
  function checkImg() {
    const $imgs = document.querySelectorAll('#box li img')
    $imgs.forEach((dom) => {
      const dataSrc = dom.getAttribute('data-src')
      // 此top为该元素距离窗口顶部距离,
      // 所以直接判断该值是否小于窗口可视区的值,即在可视区内
      const { top } = dom.getBoundingClientRect()
      // 先判断有没有data-src,防止不断修改图片src
      if (dataSrc && (top < window.innerHeight)) {
        dom.src = dataSrc
        dom.removeAttribute('data-src')
      }
    })
  }
  window.addEventListener('scroll', throttle(function(ev) {
    // 距离底部还有20px
    const { scrollTop, clientHeight, offsetHeight } = document.documentElement
    if (offsetHeight - ( scrollTop + clientHeight ) < distance) {
      initImg()
    }
    checkImg()
  }, 200))
  // 进行首次的资源获取
  initImg().then(() => {
    checkImg()
  })
}
</script>
<div id="box">
  <ul></ul>
</div>
</body>
</html>

除此之外还有个 api 专门来处理这种可视回调的IntersectionObserver
具体可参阅下阮老师的文章

自己实现call,apply,bind

介绍

这三个都是改变函数里的 this 指向
第一个参数都是用来替换 this 的对象,后面是原来函数的参数,不定个数
当第一个参数为 null 或 undefined,则里面 this 指向 window

call,apply用法一样,区别在于

func.call(obj, a, b, c, ...) // 后面的参数是一个一个
func.apply(obj, []) // 后面的参数是一个数组
// 这里有个帅气的找出数组最大值
Math.max.apply(null, [1,2,3,4,1,2,3])

bind 比较特别,返回的是个函数,不执行

// 后面的参数也是一个一个,同时会被锁住,成为原函数的前几个参数
const newFn = bind.call(obj, a, b, c, ...)
// 再调用的时候,参数为原函数的第n+1个参数
newFn(e, f, g, ...)

上码

var name = 'windowname'
var age = 'windowage'  
const boy = {
  name: 'xiaoming',
  age: 18,
  show (a, b, c) {
    console.log(`${this.name} is ${this.age}`, a, b, c)
  }
}
const girl = {
  name: 'xiaohong',
  age: 16
}
boy.show(7, 8, 9) // xiaoming is 18 7 8 9

// 先来正常的结果
console.log('------------------- 对比 -------------------')
boy.show.call(girl, 17, 18, 19, 10) // xiaohong is 16 17 18 19
boy.show.apply(girl, [27, 28, 29, 20]) // xiaohong is 16 27 28 29
boy.show.call(null, 17, 18, 19, 10) // windowname is windowage 17 18 19
boy.show.apply(undefined, [27, 28, 29, 20]) // windowname is windowage 27 28 29

const bindFn1 = boy.show.bind(girl, 17, 18, 19, 10)
bindFn1() // xiaohong is 16 17 18 19
bindFn1(1, 2, 3) // xiaohong is 16 17 18 19

const bindFn2 = boy.show.bind(girl, 27)
bindFn2() // xiaohong is 16 27 undefined undefined
bindFn2(1, 2, 3) // xiaohong is 16 27 1 2

const bindFn3 = boy.show.bind(null, 27)
bindFn3() // windowname is windowage 27 undefined undefined
bindFn3(12, 13, 14) // windowname is windowage 27 12 13

console.log('------------------- 分割线 -------------------')
// 通俗来讲就是哪个对象调用了函数,this就指向谁,当前面没有谁调用则是window
// 绑定对象赋予一个字段为函数,来调用这个函数
// 当绑定对象为 null 或者 undefined,则让这个函数单独运行,自然 this 会指向window
// 随机数的为了不要不小心覆盖到绑定对象原来的属性
// 不能绑定后对象多了个新属性,要删掉多出来的字段函数,雁过不留痕
Function.prototype._call = function(context, ...param) {
  // 随便的随机名字
  const functionName = Math.floor(Math.random() * 100000)
  let result
  if (context) {
    context[functionName] = this
    result = context[functionName](...param)
    delete context[functionName]
  } else {
    result = this(...param)
  }
  return result
}

// 当不能用 ES6 的解构时候,就用字符串拼接起来,然后用 eval 触发
Function.prototype._call2 = function() {
  const functionName = Math.floor(Math.random() * 100000)
  const params = []
  let _context
  for (let i = 1, l = arguments.length; i < l; i++) {
    params.push(arguments[i])
  }
  if (arguments[0]) {
    _context = arguments[0]
    _context[functionName] = this
    paramStr = `_context[functionName](${params.join(', ')})`
  } else {
    _context = this
    paramStr = `_context(${params.join(', ')})`
  }
  const result = eval(paramStr)
  delete _context[functionName]
  return result
}

Function.prototype._apply = function(context, arr) {
  const functionName = Math.floor(Math.random() * 100000)
  let result
  if (context) {
    context[functionName] = this
    result = context[functionName](...arr)
    delete context[functionName]
  } else {
    result = this(...arr)
  }
  return result
}

Function.prototype._bind = function(context, ...oldParam) {
  const fn = this
  if (context) {
    return (...newParam) => {
      const functionName = Math.floor(Math.random() * 100000)
      context[functionName] = fn
      const result = context[functionName](...oldParam, ...newParam)
      delete context[functionName]
      return result
    }
  }
  return (...newParam) => {
    return fn(...oldParam, ...newParam)
  }
}

boy.show._call(girl, 17, 18, 19, 10)
boy.show._apply(girl, [27, 28, 29, 20])
boy.show._call(null, 17, 18, 19, 10)
boy.show._apply(undefined, [27, 28, 29, 20])

const _bindFn1 = boy.show._bind(girl, 17, 18, 19, 10)
_bindFn1()
_bindFn1(1, 2, 3)

const _bindFn2 = boy.show._bind(girl, 27)
_bindFn2()
_bindFn2(1, 2, 3)

const _bindFn3 = boy.show._bind(null, 27)
_bindFn3()
_bindFn3(12, 13, 14)

关于缓存的几个关键词

缓存可以看成浏览器开辟了一个区域来放缓存的东西。有些放在内存有些放在硬盘。
从缓存返回的状态码为304。缓存分两种

强缓存:

设定一个期限,期限的重复访问都会去缓存获取,有两个字符来表示这个期限:
__http1__的是__Expires__,Expires: Wed, 22 Oct 2018 08:41:00 GMT表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。
__http1.1__的是__Cache-Control__,当Cache-Control:max-age=300,表示5分钟缓存时间。还有其他几个值。直接看另外一个比较有影响的:no-cache。这个看做就是不强缓存,继续去判断协商缓存。

Cache-Control优先级高于Expires,现在基本都是http1.1,Expires也就是兼容用。

协商缓存:

首次访问资源,响应头会返回标识符(__Last-Modified/ETag__),下次访问请求头会带上标识符(__If-Modified-Since/If-None-Match__),去服务器查询是否启用缓存。也就是请求还是要发,但是至少东西是省发了。

响应头和请求头的缓存标识符字段名不一样,但值一样。

Last-Modified和If-Modified-Since,就是资源最后修改时间,发送到服务器去判断有没有更新,无则返回304。
缺点就很明显,如果文件有被打开或者别的什么改动了时间,那缓存失效。同时最低也是精确到秒,一秒内的改动判断不出来。

ETag和If-None-Match
Etag可以看成根据文件算出的哈希,唯一的,文件一改肯定就改。
缺点就是要消耗资源去计算。

当然ETag明显精度高于Last-Modified,所以优先级也是ETag高。

应用中的策略

设定一个期限很大的强缓存,如 max-age=31536000 (一年)。文件通过打包工具生成出来都带上哈希。只要文件内容不改哈希就不改,请求的地址就不改,命中强缓存。改了内容自然连请求地址都改了,就会返回新文件。

代码的角度,

看到的资料几乎都是服务端的,比如node的express可以在请求资源的返回里设置响应头

app.get('/foo.js', function (req, res) {
  res.set({
    'Content-Type': 'text/javascript',
    'Cache-Control': 'no-cache',
    'ETag': '12345'
  })
  res.sendFile('foo.js')
})

当然每个都这样搞未免太麻烦了,在静态资源那里可以统一设置

const options = {
  dotfiles: 'ignore',
  etag: false,
  extensions: ['htm', 'html'],
  index: false,
  maxAge: '1d',
  redirect: false,
  setHeaders: function (res, path, stat) {
    res.set('x-timestamp', Date.now())
  }
}

app.use(express.static('public', options))

上面是默认设置,自己也可以修改。

至于前端的设置,似乎只有

<meta http-equiv="expires" content="Wed, 20 Jun 2007 22:33:00 GMT">
<meta http-equiv="cache-control" content="no-cache">

尝试设置为强缓存好像没生效。而且这还是只是针对该份html,其他的资源似乎无能为力,可能也是了解不够。

其他

上面提到的利用打包工具来做这个哈希名字的事,首先想到的当然是webpack

module.exports = {
  output:{
    path:path.join(__dirname, '/dist/js'),
    filename: '[name].[chunkhash].js',
  }
}

都知道输出文件名字可配置成带哈希字符的,也就是上面的 [chunkhash] 部分,这里有三个值可以写:
hash、chunkhash、contenthash

__hash:__利用整个工程内容来计算,所以改一点点全部都改。
__chunkhash:__计算的单独chunk(这个是webpack的概念,相当于每个要输出的文件,一般一个entry对应一个chunk)的哈希。
__contenthash:__在使用抽离的css插件的时候,抽离的css文件会与引用它的js共用一份chunkhash,那js一改css也得改。当然反过来也是。此时用contenthash就只针对该份文件内容做哈希,就不会互相影响到了。