标签 JavaScript 下的文章

好多小伙伴都看到过这样一段全是由各种括号,以及有限的几个操作符组成的代码,完整代码贴在下面。为了好看,我给它格式化了一下:

[]
[
    (![] + [])[+[]] + 
    ([![]] + [][[]])[+!+[] + [+[]]] +
    (![] + [])[!+[] + !+[]] +
    (!![] + [])[+[]] +
    (!![] + [])[!+[] + !+[] + !+[]] +
    (!![] + [])[+!+[]]
]
[
    (
        [][
        (![] + [])[+[]] +
        ([![]] + [][[]])[+!+[] + [+[]]] +
        (![] + [])[!+[] + !+[]] +
        (!![] + [])[+[]] +
        (!![] + [])[!+[] + !+[] + !+[]] +
        (!![] + [])[+!+[]]] + []
    )[!+[] + !+[] + !+[]] +
    (
        !![] +
        [][(![] + [])[+[]] +
        ([![]] + [][[]])[+!+[] + [+[]]] +
        (![] + [])[!+[] + !+[]] +
        (!![] + [])[+[]] +
        (!![] + [])[!+[] + !+[] + !+[]] +
        (!![] + [])[+!+[]]]
    )[+!+[] + [+[]]] +
    ([][[]] + [])[+!+[]] +
    (![] + [])[!+[] + !+[] + !+[]] +
    (!![] + [])[+[]] +
    (!![] + [])[+!+[]] +
    ([][[]] + [])[+[]] +
    (
        [][(![] + [])[+[]] +
        ([![]] + [][[]])[+!+[] + [+[]]] +
        (![] + [])[!+[] + !+[]] +
        (!![] + [])[+[]] +
        (!![] + [])[!+[] + !+[] + !+[]] +
        (!![] + [])[+!+[]]] + []
    )[!+[] + !+[] + !+[]] +
    (!![] + [])[+[]] +
    (
        !![] +
        [][(![] + [])[+[]] +
        ([![]] + [][[]])[+!+[] +
        [+[]]] +
        (![] + [])[!+[] + !+[]] +
        (!![] + [])[+[]] + (!![] + [])[!+[] + !+[] + !+[]] +
        (!![] + [])[+!+[]]]
    )[+!+[] + [+[]]] +
    (!![] + [])[+!+[]]
]
(
    (![] + [])[+!+[]] +
    (![] + [])[!+[] + !+[]] +
    (!![] + [])[!+[] + !+[] + !+[]] +
    (!![] + [])[+!+[]] +
    (!![] + [])[+[]] +
    (
        ![] +
        [][
        (![] + [])[+[]] +
        ([![]] + [][[]])[+!+[] + [+[]]] +
        (![] + [])[!+[] + !+[]] +
        (!![] + [])[+[]] +
        (!![] + [])[!+[] + !+[] + !+[]] +
        (!![] + [])[+!+[]]
        ]
    )[!+[] + !+[] + [+[]]] +
    [+!+[]] +
    (
        !![] +
        [][(![] + [])[+[]] +
        ([![]] + [][[]])[+!+[] + [+[]]] +
        (![] + [])[!+[] + !+[]] +
        (!![] + [])[+[]] +
        (!![] + [])[!+[] + !+[] + !+[]] +
        (!![] + [])[+!+[]]]
    )[!+[] + !+[] + [+[]]]
)
();

这是一坨什么玩意儿?看得人脑瓜子嗡嗡的。
今天上班第一天,不想干活,只想摸鱼。趁着这个时间娱乐一下,看看这段代码到底是什么东西。

从上面的代码中可以看出来,我已经把它的主要结构展示地非常清晰:

//① ②    ③    ④   ⑤
 [][...][...](...)();

非常明显,第一个[]肯定是提供了一个存在于它的原型链上的 方法 或者 属性 供第二个[]使用。既然是用方括号调用方法或者属性,那么第二个[]里面必定是一个字符串,就像这样:

[]['map'];
//等价于
Array.prototype.map;

当然,第二个[]内的字符串不一定是map,我只是用map举个栗子,具体内容还要进一步分析。

以此类推,第三个[]类似第二个[],举例来说就是这样:

[]['map']['name'] //'map'

上面那行示例代码获取到了函数Array.prototype.mapname属性,但本文讨论的代码究竟获取到了什么东西,还是留待进一步分析。

咱们继续往下看。第四部分是一个小括号()。小括号除了分块外,最大的作用是调用函数。综合上文的分析,第三部分应该是返回一个方法,而第四部分的这个小括号就是调用第三部分返回的方法,小括号里面的内容呢,那肯定就是参数了。

至于第五部分也就是最后一个小括号,很明显是调用方法。既然这样,那么前四部分整体返回的就是一个方法,也就是说,第四部分执行了一个返回方法的方法。

什么方法会返回一个方法呢?继续深入分析。

第一部分[]是一个空的数组实例,除了继承了Array的一些属性和方法,其他没什么可分析的。

第二部分是由5个+操作符连接的语句。下面以第二部分的第一行为例进行分析。
第一行是(![] + [])[+[]],又可以分解成(![] + [])[+[]],其中涉及到的知识就是隐式转换。其中:

![]
//等价于
!''

结果是false,于是

(![] + [])
//等价于
(false + [])
//等价于
(false + '')
//等价于
('false' + '')
//等于
'false'

隐式转换的具体规则网上已经有很多文章了,这里就不重复了。

[+[]]
//等价于
[+'']
//等价于
[0]

所以,第一行最后的结果是

'false'[0]
//等于
'f'

搞了半天,原来是为了获取'f'这个字符。

实际上,分析完上面这行代码,其他的就不言自明了,套路都是一样的,第二部分和第三部分的目的,是为了拼凑'filter''constructor'这两个字符串。

经过分析,整个代码相当于:

[]['filter']['constructor']('alert(1)')();

等于

[].filter.constructor('alert(1)')()

那么为啥拼凑'filter'这个字符串,而不是拼凑'map''sort'……呢?这里必须要好好说道说道,这是因为,'filter'这个字符串,它容易得到……

[].filter.constructor指向的是构造函数Function,利用它,可以使用字符串构造函数,类似于eval(),所以[].filter.constructor('alert(1)')相当于

Function('alert(1)')

执行Function('alert(1)'),就会弹出 1 了:

Function('alert(1)')()

好了,本文到此结束了。


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