标签 JavaScript 下的文章

对象和数组

利用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模板部分:

{{computedValue}}

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可能会没有应有的样式,例如没有下划线,看起来就跟普通文本无异。如果仅仅这样写:
link
又相当于

点击之后会刷新当前页面。
好多开发者喜欢写上一个#来表示这是一个空链接:
link
这样倒是没有上面的问题,但又有新问题:页面会跳转到顶部。很多大网站都没有注意到这一点。
其实解决这个问题的方法很简单,只需要在#后面再添加一个不存在的锚点,点击a的时候就没有任何反应了:
link
简洁点也可以这样写:
link
link
全凭你的喜欢好了。

不过这样也有点美中不足:点击之后,虽然页面没有反应,但会在浏览器的地址栏出现你设置的锚点。其实这无关紧要。不过如果你非要觉得别扭,那下面的方法可以解决这个小小的不足:
link
void总会返回undefined,你在void之后写啥都没关系。这样写除了要多输入几个字符,就没什么问题了。

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

如果你想这么写也不是不可以:
link
link
link
link
link
但是这样用要小心!虽然在一些浏览器里毫无反应,但是在Firefox里,页面会被表达式的值替换
至于类似下面这样的,千万别用就对了:
link
link
如果你好奇地试了,那你的页面就会被替换成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