标签 JavaScript 下的文章

一、window.devicePixelRatio

很多文章都是介绍这个方法来检测网页是否被缩放,因为在PC上,页面的devicePixelRatio一般是1。很多网站也是用的这个方法:

if(window.devicePixelRatio !== 1){
    alert('页面处于缩放状态');
}

然而这种方法并不准确。现在的显示设备大都拥有高分辨率,如果使用 Windows 默认的缩放级别,字体会过小,可能看不清,所以通常用户会把 Windows 的缩放级别调整为 1.25 或更高:

1.png

这种情况下,window.devicePixelRatio就不再等于1

2.png

所以,应该寻找一种更准确的办法。

二、window.outerWidth 与 window.innerWidth

window.innerWidth是网页可见区域的宽度,它有一个重要的特点:值会随着页面的缩放变化。页面放大的时候,虽然文字、图片变大了,但我们能看到的内容变少了,也就是可视范围变小了,所以window.innerWidth的值会变小,反之同理。

window.outerWidth是浏览器窗口的宽度,包含浏览器的边框。窗口在最大化状态下,浏览器左右两边是没有边框的,这个时候,这两个值是相等的:

666.png

在这种情况下,根据window.innerWidth的特性,如果window.outerWidthwindow.innerWidth的值不相等,就可以判断浏览器处于缩放状态。但是,在窗口非最大化的情况下,即使页面没有缩放,这两个值也会有很小的差值,就是两边浏览器边框的宽度,我们需要处理这种情况。

窗口在非最大化的状态下,左右会有比较细的边框,不同的操作系统下,不同的浏览器,边框的宽度都会有些不同。目前在 Windows 10 下主流的浏览器左右边框的宽度都是比较小的,例如 Chrome,边框只有一条线,可能只是 1 像素:

3.png

可以假定一个阈值,例如,如果两个值的差小于 10,我们就不认为浏览器处于缩放状态,反之则认为浏览器处于缩放状态。代码如下:

function isZoom(){
    const outerWidth = window.outerWidth,
          innerWidth = window.innerWidth;

    if(outerWidth - innerWidth > 10) {
        return alert('放大');
    }

    if(innerWidth - outerWidth > 10) {
        return alert('缩小');
    }

    alert('正常');
}

注意:10是我随意定的一个数值,并不严谨,需要调研各种浏览器的边框宽度才能得出较准确的数值。

调整浏览器缩放,得到结果如下:

放大状态检测:

4.png

正常状态检测:

5.png

缩小状态检测:

6.png

End

对象和数组

利用Object()方法来判断一个变量是否为对象:

如果Object方法的参数是一个对象,它总是返回该对象,即不用转换。这一点可以用来判断变量是都是对象。

var obj = {};
var isObject = obj === Object(obj);

利用Object.prototype.toString()方法判断变量类型

toString方法返回变量的类型字符串,可以用来判断变量类型。由于数组、字符串、函数、Date等对象(构造函数)都自定义了自己的toString方法,所以要使用Object.prototype.toString()

Object.prototype.toString.call(2); // "[object Number]"
Object.prototype.toString.call(''); // "[object String]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(Math); // "[object Math]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call([]); // "[object Array]"

这个方法比typeof更准确,例如:

typeof []; //'object'

利用Array.isArray()判断变量是否为数组

同样的,这个方法也可以弥补typeof的不足:

typeof []; // 'object'
Array.isArray([]); // true

关于数组的实例方法concat()

“如果数组成员包括对象,concat方法返回当前数组的一个浅拷贝。所谓“浅拷贝”,指的是新数组拷贝的是对象的引用。”
https://wangdoc.com/javascript/stdlib/array.html

确切的意思是,如果数组成员包括对象,concat方法返回的新数组中,对象成员是原数组对象成员的引用:

var arr = [1, 2, {a: 3}];
var arr1 = arr.concat(4); // [1, 2, {a: 3}, 4]
console.log(arr1[2]); // {a: 3}
arr1[2].a = 5;
console.log(arr[2]);  // {a: 5}

会改变原数组的方法

pop push shift unshift reverse splice, sort

slice方法

1、不传参数时,会返回原数组的拷贝

[1, 2, 3].slice(); // 返回原数组的拷贝 [1, 2, 3]

同样的,对于包含对象成员的数组,返回的新数组中的对象仍然是原数组的浅拷贝,也就是引用。

2、可以将类数组对象转换成整整的数组

var arrLikeObj = { 0: 1, 1: 2, length: 2};
Array.prototype.slice.call(arrLikeObj); // [1, 2]

类数组对象,就是具有数字类型的键值,又具有length属性的对象。

利用splice方法插入新元素

splice方法的第二个参数是0时,表示只插入新元素,不删除任何元素:

var arr = [1, 2, 3];
arr.splice(0, 0, 0);
console.log(arr); // [0, 1, 2, 3]

async 函数适合执行一些流程,不要试图使用 async 函数返回一些东西。有一个错误的例子就是在Vue的模板中绑定一个计算属性,这个计算属性里面使用 async 函数获取数据。示例代码如下:

HTML模板部分:

<div>{{computedValue}}</div>

JavaScript部分:

//......
computed: {
    async computedValue(){
        let val = await aAsyncMethod();
        return val;
    }
}

如果你这么做了,你永远只会得到如下字符串:

'[object Promise]'

为什么?

原因很简单,就像 MDN 上这篇文档 MDN - async函数 介绍的,async 遇到await的时候会立即返回一个Promise,把控制权交出,直到 await 后面的异步操作有结果(resolve或者reject)才会继续执行后面的代码。可以说,async 函数的返回值永远是一个Promise

END

有时候我们需要从后端接口返回的二进制流数据中读取文件内容。在控制台中查看二进制流,会看到下面这样的东西:

1.jpg

这就需要前端读取二进制数据。

使用fileReader

如果你要将获取到的流显示出来,比如一张图片,那么一般都是通过将二进制流转换为blob,然后使用fileReader读取DataURL,并将它赋值给imgsrc

axios和请求一个图片为例。
首先,如果请求获取的是二进制数据,需要设置请求类型:

request({
    //...
    responseType: 'arraybuffer'
    //...
})

获取到数据后,将接口返回的数据转换成blob

request({...}).then(r => {
    let myblob = new Blob([r], { type: 'image/*' })
})

接着,使用fileReader读取blob

request({...}).then(r => {
    let myblob = new Blob([r], { type: 'image/*' });
    let reader = new FileReader();
    reader.readAsDataURL(myblob);
    reader.onload = function () {
        console.log(reader.result);
    };
})

以上代码将会打印出下面这种字符:

data:base64; image/*, foiHirf89hHoheruiefhi......

这样的Data URL可以直接用在imgsrc属性上。

fileReader还提供了readAsArrayBuffer()readAsText()等方法,用来读取不同类型的内容。

使用URL.createObjectURL

如果你想把流下载下来,那么使用此方法比较合适。
使用URL.createObjectURL(blob)可以创建一个指向blob内容的url。获取blob的步骤和之前一样,获取到 blob 后,使用下面的代码创建 ObjectURL:

const blob = ......;
const url = URL.createObjectURL(blob);
//生成的url如下:
'blob:http://localhost:9999/0a147e01-97a4-482a-8aa3-e44a2bf79478''

之后,需要借助adownload属性,下载这个文件:

var link = document.createElement('a');
link.href = url;
link.download = 'fileName.jpg';
document.body.append(link);
link.click();
link.remove();

此时文件已经自动下载了。注意,adownload属性要设置正确的带扩展名的文件名,这样才能正确下载文件,而不必设置文件的mime
最后,别忘了调用如下方法释放资源:

URL.revokeObjectURL(url);

END

我是不是相亲相爱一家人朋友圈看多了,竟然起了个这样的标题……咳咳,言归正传

其实这种情况只存在嵌套循环中。

下面两个数组中,obj1某一项的id等于obj2某一项的xid,想要通过循环把obj2每一项的value赋给obj1中与之对应的项:

const obj1 = [
  { id: 1, ... },
  ...
];

const obj2 = [
  { xid: 1, value: 1, ...},
  ...
]

不知道为什么,下面这种写法,外层循环只能循环到i = 3就不往下执行了:

let i = 0, j = 0, lenI = obj1.length, lenJ = obj2.length;

for (; i < lenI; i++) {
  for (; j < lenJ; j++) {
    if (obj1[i]['id'] === obj2[j]['xid']) {
      obj1[i]['value'] = obj2[j]['value'];
      break;
    }
  }
}

下面这样写就没问题:

for (let i = 0, len_i = obj1.length; i < len_i; i++) {
  for (let j = 0, len_j = obj2.length; j < len_j; j++) {
    if (obj1[i]['id'] === obj2[j]['xid']) {
      obj1[i]['value'] = obj2[j]['value'];
      break;
    }
  }
}

为啥呢?原因很简单,当内层循环执行的时候,j的值已经改变了,前几次循环因为触发了breakj的值还小于lenJ,所以还能继续执行,直到j == lenJ,内层循环就不会再执行了。

这个错误好愚蠢……

END

众所周知,<a>href属性除了能指向一个URL,还能指向锚点。锚点链接就不说了,这里说说它的特殊用法。

在初期开发,尤其是切图的时候,需要给a一个href属性,否则a可能会没有应有的样式,例如没有下划线,看起来就跟普通文本无异。如果仅仅这样写:

<a href>link</a>

又相当于

<a href="当前页面的URL"></a>

点击之后会刷新当前页面。
好多开发者喜欢写上一个#来表示这是一个空链接:

<a href="#">link</a>

这样倒是没有上面的问题,但又有新问题:页面会跳转到顶部。很多大网站都没有注意到这一点。
其实解决这个问题的方法很简单,只需要在#后面再添加一个不存在的锚点,点击a的时候就没有任何反应了:

<a href="#idonotcare">link</a>

简洁点也可以这样写:

<a href="##">link</a>
<a href="#?">link</a>

全凭你的喜欢好了。

不过这样也有点美中不足:点击之后,虽然页面没有反应,但会在浏览器的地址栏出现你设置的锚点。其实这无关紧要。不过如果你非要觉得别扭,那下面的方法可以解决这个小小的不足:

<a href="javascript:void(0)">link</a>

void总会返回undefined,你在void之后写啥都没关系。这样写除了要多输入几个字符,就没什么问题了。

但是,在鼠标放到这个链接上时,浏览器下面还是会显示这个链接的地址:javascript:void(0)。比如在Chrome中,对于追求完美的你来说,显示地址的小白条会破坏页面的整体感,这时候,你就只好用设置a的默认样式+去掉href属性+绑定click的方法了……

如果你想这么写也不是不可以:

<a href="javascript:undefined">link</a>
<a href="javascript:false">link</a>
<a href="javascript:1">link</a>
<a href="javascript:1+1">link</a>
<a href="javascript:function f(){}">link</a>

但是这样用要小心!虽然在一些浏览器里毫无反应,但是在Firefox里,页面会被表达式的值替换
至于类似下面这样的,千万别用就对了:

<a href="javascript:document.write('0')">link</a>
<a href="javascript:document.title">link</a>

如果你好奇地试了,那你的页面就会被替换成0或者网页标题,耶稣都救不了你,我说的~

END

1、如何使用

很多场景,比如给ajax请求传递参数时,参数往往是一个对象,对象的属性个数不是固定的,需要动态添加:

let params = {
    id: 1
};

if(name){
    params.name = name;
}

if(address){
    params.address= address;
}

现在ES6提供了扩展运算符,以上的代码可以改成下面这样:

let params = {
    id: 1,
    ...(name && {name}),
    ...(address && {address}),
}

如果nameaddress的值都存在且不是空字符串,结果就会是这样:

{
    id: 1,
    name: 'dong',
    address: '赤水沟子'
}

如果它们的值为,就会不存在nameaddress这两个属性:

{
    id: 1
}

2、原理

下面以name为例,说明它的原理。

如果name不为空,则

name && {name}

相当于

name && {name: name} //上面是ES6简写形式

相当于

if(name){
    return {
        name: name
    }
}else{
    return false;
}

如果name没有值,那么

...(name && {name})

相当于

...(false)

扩展运算符有以下规定:

1、如果扩展运算符后面是一个空对象,则没有任何效果
2、如果扩展运算符后面不是对象,则会自动将其转为对象

根据第2点,

...(false)

等同于

...Object(false)

等同于

...Boolean {false}

Boolean {false}没有任何属性,所以又等同于

...{}

根据第1点可以知道,这没有任何效果,不会向params添加任何属性。

END

经常看到这样的JavaScript代码:

var o = {
    f: function(){
        console.log(this);
    }
}

(0, o.f)();

这样做的目的是改变f的执行环境,也就是改变this。

直接调用o.f:

o.f();
//{f: ƒ}

再试试间接调用:

(0, o.f)();
//Window...

可以看出this已经是Window了。

逗号运算符会从左往右求值,并返回最后一个表达式的值,(0, o.f)返回的是一个函数,此时这个函数的作用域是全局,相当于:var f = o.f
必须要用0吗?其实任何表达式都可以:

(1, o.f);
(1+1, o.f);
(true, o.f);

()是不是必须的呢?要调用返回的函数,或者将返回的函数赋值给某个变量,括号就是必须的。下面的代码肯定是不能正确运行的:

0, o.f(); //this指向o
var f = 0, o.f //Unexpected token '.'

我们需要()来将语句转换为表达式,这也是经常用到的方法,比如下面的代码,需要将x = 2这条语句转换为表达式:

true && (x = 2)

END

JavaScript获取时间戳有多种方式。这里有一个具有纪念意义的时间:

var now = new Date('2022/02/02 22:22:22:222');
//Wed Feb 02 2022 22:22:22 GMT+0800 (中国标准时间)

我们想看看它的时间戳是不是也如此独特。下面是如何获取它的时间戳。

方法1:Dateparse方法

Date.parse(now);
//1643811742000

Date.parse接受一个表示时间的字符串,所以这里的now先转换成了字符串Wed Feb 02 2022 22:22:22 GMT+0800 (中国标准时间),然后再被解析成时间戳。因为每个浏览器实现它的方式不一致,所以建议不要用Date.parse解析时间字符串。见:Date.parse() - MDN
要注意的是,此方法返回的毫秒数都是0.

方法2:Date实例getTime方法

now.getTime();
//1643811742222

这个方法实际上跟方法1是等价的,方法1是显式调用,方法2是隐式调用,但此方法返回正常的毫秒数。

方法3:Date实例的valueOf方法

now.valueOf();
//1643811742222

这个方法返回一个Date对象的原始值。Date对象的原始值是基于Unix Time Stamp,即自1970年1月1日(UTC)起经过的毫秒数。

方法4:+操作符获取时间戳

+now;
//1643811742222

这个方法实际上调用的是now.valueOf()
+运算符在这里是一元运算符,会将后面的操作数转换成数字,等于Number()函数。数字是一种原始值。在JavaScript内部,会调用toPrimitive把对象转换成原始值,形式如下:

toPrimitive(input,preferedType?)

preferedType可选numberstring+会将preferedType看作是number,将按照下面的规则转换:

  1. 如果input是原始值,直接返回这个值;
  2. 否则,如果input是对象,调用input.valueOf(),如果结果是原始值,返回结果;
  3. 否则,调用input.toString()。如果结果是原始值,返回结果;
  4. 否则,抛出错误。

+now按照规则2调用了now.valueOf()
注意,Date.prototype.valueOf重写了Object.valueOf,所以会返回一个时间戳。

结果证明now的时间戳并不独特:(

END