1. 神乎其技的 + 号
//使用 `+` 运算符可以快速将一个字符串数值转化为数字
console.log(typeof '1'); //string
console.log(typeof +'1'); //number
//Date类型会转化为number类型的时间戳,精确到ms
console.log(+new Date()) //1484219585488
//相比这种方式转换时间戳简便,Date.parse()只精确到s,ms的位置都为0
console.log(Date.parse(new Date())) //'1484219585000'
//无法转换为有效的数字一般会得到NaN
console.log(+'abc') //NaN
console.log(+function(){}) //NaN
//数组有点奇特
console.log(+[1,2,3]); //NaN
console.log(+[]); //0
console.log(+[5]); //5
//这还就导致了
console.log(++[[]][+[]]+[+[]]) //10
//为什么呢?一脸懵逼的你一定愿意看看推导
+[] = 0
=> ++[[]][+[]]+[+[]] = ++[[]][0]+[0]
[[]][0] = []
=> ++[[]][0] = ++[] = [] + 1 = '' + '1' = '1'
//++运算符得到的结果一定是number,所以有必要用 + 号再一次转换类型
++[] = +( [] + 1 ) = +'1' = 1
=> 1 + [0] = '1' + '0' = '10'
2. 妙用数组length属性
var a = [1, 2, 3, 4];
console.log(a.length) //4
//清空数组
a.length = 0; //a = []
//截取数组
a.length = 2; //a = [1, 2]
//扩张数组,用undefined填充
a.length = 5; //a = [1, 2, 3, 4, undefined]
3. 两个数花式交换
//方案一
var a = 1, b =2;
a = [b, b = a][0];
console.log(a, b); //2 1
//方案二
var a = 1, b =2;
a = a ^ b;
b = a ^ b;
a = a ^ b;
console.log(a,b) //2 1
4. 在读取length的循环中缓存length
var a = [1, 2, 3];
for(var i = 0; i < a.length; i++){
console.log(a[i])
}
尽管上面的做法没有异议,但每次循环都会额外做一个计算数组length的操作,数组足够小的时候这当然没有任何问题,但当它足够大,它就会成为拖垮性能的一个元凶。假如你尝试过在java中处理一个超大文件的每一个字节时使用上面的做法,你可能会懂得它对性能的破坏力有多大。
必要的时候采用下面这种做法吧:
var a = [1, 2, 3];
for(var i = 0, len = a.length; i < len; i++){
console.log(a[i]);
}
5. 可动态指定地访问Object属性
var o = {
name : 'cmx',
age : 24
}
//可以这么访问一个key
console.log(o.name);
//还可以这么访问一个key
console.log(a['name']);
//再直白一点
var key = 'name';
console.log(a[key]);
看完你能知道应用场景吗?
6. 妙用And和Or
//设置变量的初始默认值
function(a){
a = a || '默认值';
}
//代替if语句,利用短路特性,如果a变量是有效值(非undefined、null、""、false、0等),执行方法dosomething
a && dosomething(a);
//同理,a为无效值时执行dosomething函数
a || dosomething();
7. 数组拼接
存在两种方式去拼接两个数组:
var a = [1,2];
var b = [3];
//生成一个新的数组,不破坏原数组
a = a.concat(b) //a=[1,2,3]
//以第一个参数数组作为this,第二个参数数组作为遍历参数,push进第一个数组,最终第一个数组为两个数组的拼接,第二个数组不改变
Array.prototype.push.apply(a, b) //a=[1,2,3] b=[3]
//相当于
a.push(b[0], b[1], b[2]..)
什么时候用哪种呢?concat
由于生成新的数组,必然是占内存的,但它不限制合并数组大小。而第二种方法存在合并数组个数限制(实际上是函数的形参个数限制,不超过65536),内存则相对于concat
减少了一个合并后数组的大小。性能上相差不多。
8. 两个操作符typeof跟instanceof
-
typeof
判断变量的类型,为一元运算符 -
instanceof
判断变量的实例(原型链),为二元运算符
用法上,
console.log(typeof {name:'chenmuxin'}) //'object'
console.log({name:'chenmuxin'} instanceof Object) //true
typeof
返回的是一个全小写字符串,依据变量的类型可能会返回:
'object'
、'number'
、'string'
、'undefined'
、'function'
、'boolean'
、'symbol'
如你所见,不存在数组类型。数组返回的是'object'
,不仅数组,所有诸如Date
、RegExp
都被作为object
看待
'number'
类型:
typeof 1
typeof(1) //也可以这样使用,类似java
typeof 3.1415 //js的number类型统一了整型浮点型等
typeof NaN //Not a number表示的是无穷和非数值,它是一个number类型
typeof Number(1) //Number类直接返回的类型也是number
'undefined'
类型:
typeof undefined
typeof i_am_undeclared //一个未声明变量
var i_am_declared_but_undefined_value; //一个已声明但未赋值变量
typeof i_am_declared_but_undefined_value
最容易迷惑情形:
typeof new Number(1) //返回'object',使用new操作符时,诸如String、Boolean等引用类返回的是object
typeof Number(1) //'number'
typeof null //返回'object',js诞生之时起,null就是跟object同类型,这显然不合适。但es6的提案typeof null == 'null'被否决,所以目前都还只能这么认识它
typeof /a/ //正则在不同浏览器上可能有不同类型,可能是function,也可能是object,标准是object。判断正则应使用instanceof
使用instanceof
时,首先要保证对象可以new
,只有允许new
才会存在所谓实例。所以想使用instanceof
判断一个基本变量的类型是不可行的。
console.log(1 instanceof Number) //false
数组无法使用typeof
来判断,但可以使用instanceof
(一定场景下会失败,例如多重iframe)
console.log([1] instanceof Array) //true
当然也可以使用更稳妥的数组的方法:
console.log(Array.isArray([1])) //true
总结:像数组、正则等继承自Object
但具体的对象(首先可以new
),我们一般都可以用instanceof
去判断。像很多基本变量以及它们的引用类型,我们就用typeof
去判断。
9. in和for遍历
var a = [10,20,30];
var b = {name : 'cmx'}
//基本数组遍历
for(var i=0,length = a.length; i < length; i++){
console.log(a[i]);
}
//of运算符每次循环直接得到数组值
for(var i of a){
console.log(i);
}
//in运算符每次循环得到当前下标
for(var i in a){
console.log(a[i]);
}
//in运算符遍历对象时,每次得到对象的key
for(var i in b){
console.log(i);
console.log(b[i]);
}
存在一些特殊情况,in
遍历时,不仅会把数组(或对象)的每一个下标(key)遍历出来,一旦像下面这样定义了原型属性或方法
Array.prototype.what = function(){ }
in
遍历还会把它们一起遍历出来,这里得到一个what
。如果没有作必要的判断往往会导致如下:
for(var i in a){
//打印a[what]出错
console.log(a[i]);
}
要么不使用原型直接添加的方式,而使用Object.defineProperty
并设置可枚举属性为false。要么就in
遍历数组时,都判断当前i是否为number类型:
var a = [1, 2, 3];
Array.prototype.cmx = 5;
for(var i in a){
//+i是因为i遍历出来的都是字符串形式。至于为什么不使用 i != NaN ,因为任意两个NaN不相等啊
if(!Number.isNaN(+i)){
console.log(i)
}
}
因为原型扩展是不允许添加一个number类型的,所以上面的做法成立。
Array.prototype.1 = 1 //error
或者使用通用的方法(同时适合数组和对象)
for(var i in a){
if(a.hasOwnProperty(i)){
console.log(a[i])
}
}
Object.prototype.hasOwnProperty()
会忽略对象原型链上的属性和方法。
10. call和apply
我们先来看段代码:
function Chen(){
this.name = '老陈';
this.sayHello = function(hello){
console.log('我是老陈的方法');
console.log(hello + ', I am ' + this.name);
}
}
function Huang(){
this.name = '老黄';
this.sayHello = function(hello){
console.log('我是老黄的方法');
console.log(hello + ', I am ' + this.name);
}
}
var chen = new Chen();
chen.sayHello('你好'); //我是老陈的方法 你好, I am 老陈
var huang = new Huang();
chen.sayHello.call(huang,'Hello'); //我是老陈的方法 Hello, I am 老黄
chen.sayHello.apply(huang,['Hi']) //我是老陈的方法 Hi, I am 老黄
call
和apply
非常像,都是为一个函数指定上下文,并传入参数。唯一的区别在于参数参入的形式。
Function.prototype.call(thisObj, ...args)
Function.prototype.apply(thisObj[, argArray])
call()
以thisObj
作为上下文带上若干个指定的参数值调用某个函数或方法,参数必须展开。
apply()
以thisObj
作为上下文带上一个数组或累数组参数调用某个函数或方法,参数必须组合成一个数组。
由于只是形式不同,故以其中一个作为示例说明:
chen.sayHello.call(huang, 'Hello')
以huang
作为上下文,调用chen
的sayHello
方法,由于chen.sayHello()
调用的this.name
已经被更改为huang.name
,所以得到上面的执行结果。
用call
或者apply
来模拟一下继承:
function Boy(name, love){
this.name = name;
this.lova = love;
this.say = function(){
console.log('I am '+ name +',I like '+ love +',what about you?');
}
}
function BadBoy(name, love){
Boy.call(this, name, love);
this.do = function(){
console.log('I made a mistake!');
}
}
var badBoy = new BadBoy('张三','打架');
badBoy.say(); //I am 张三,I like 打架,what about you?
badBoy.do(); //I made a mistake!
var boy = new Boy('李四','学习');
boy.say(); //I am 李四,I like 学习,what about you?
boy.do(); //boy.do is not a function
(转载请注明出处,简书-沐心chen)