类型、语法、异步

123 阅读9分钟

你不知道的javascript(中)

一、类型

内置类型

七种:null, undefined, boolean, number, string, object, symbol.

除object外,其他统称基本类型。

typeof可以检查类型值,返回的是类型的字符串值。

console.log(typeof undefined);//undefined
console.log(typeof true);//boolean
console.log(typeof 42);//number
console.log(typeof "11");//string
console.log(typeof { name: "Mike" });//object
console.log(typeof symbol());//symbol

//几个特殊的
console.log(typeof null);//object
console.log(typeof function a() {});//function
console.log(typeof [1, 2]);//object

null的正确返回结果应该是null。但这个bug由来已久,很难修改。检测判定为object。

function和array都是object的子类型。其拥有length等属性。typeof可以检查出来function,无法识别array。

简单类型

number:二进制浮点数。

0.1+0.2===0.3为false。

解决方案1是tofixed();

//保留小数,注意toFixed是转换成string
(0.2 + 0.1).toFixed(3) === (0.3).toFixed(3)//true
//转换成整数,先乘10^n次,再除以10^n次。
(0.2 * 10 + 0.1 * 10) / 10 === 0.3//true

undefined:声明但从未赋值;

null:曾赋值过,但是目前没有值。

string:字符串的相加会是直接排火车一样的拼接字符。

复杂类型

变量传递的时候,简单类型都是通过值的复制来赋值;复杂类型(对象、函数、数组)通过复制的方式来赋值,都是同一个引用值。

typeOf()可以检查简单数据类型和函数;准确识别各种类型需采用Object.prototype.toString()。

console.log(Object.prototype.toString.call([1, 2]));//[object Array]

在toString方法被调用时,会获取this指向的那个对象的[[Class]]属性的值。(这也是我们为什么要用call改变this指向的原因)

function classof(o) {
  if (o === null) return "null";
  if (typeof o !== "object") return typeof o;
  else
    return Object.prototype.toString
      .call(o)
      .slice(8, -1)
      .toLocaleLowerCase();
}

类型转换

  1. JSON字符串化

JSON.stringify()将数据类型转换为字符串类型。详见JSON

  1. 将数字转换为字符串:number+"";
  2. 字符串转换为数字:parseInt(string);
  3. 下列情况会进行隐式转换成布尔值:
  • if,for,while等三者的判断表达式
  • 三元表达式 ?:
  • 逻辑运算符||和&&左边的操作数等

0,"",null,undefined,转换为false。

[],{},“0”转为true

\

  1. ||和&&

返回的不是布尔值而是两个操作数当中的一个。可以称为操作数选择器。

对||来说,如果第一个条件判断为true,则返回第一个操作数的值;否则返回第二个操作数的值;

对&&来说,如果条件判断为true,则返回第二个操作数的值;否则返回第一个操作数的值;

let a = 42;
let b = "abc";
a||b    //42
//相当于
a ? a: b
a&&b    //abc
//相当于
a ? b: a
  1. 相等
//字符串与数字之间的相等比较
let a = 2
let b = "2"
a===b //false,严格相等,类型也得一样才行
a==b //true,抽象相等,类型可以不一样

//与布尔值之间的相等比较,会将布尔值转换为0,1
let c = true;
c == a //false,不能转换为布尔值来判断,得转换成数字来判断。1!==2

//null和undefined之间的比较
null == undefined //true,但和false,0,""不等。

语法

语句和表达式

表达式相当于短语,运算符相当于连接词,语句相当于一整个句子。

let a = 42;
let b = 3*6;
let c = a;
//等号右侧的“42”,“3*6”,“a”都是表达式
//三行中每一行都是一个语句。

语句都有结果值,{...}的结果值为最后一个语句。可以在控制台中看见结果值。

var b;
if (true) {
  b = 4 + 38;
}
//控制台输出42

\

副作用:同时会对其他值产生影响。例如:函数,++,- -等

let a = 42;
let b = a++;
console.log(a);//42
console.log(b);//43
//++在变量后面时,先赋值,再++;
//++在变量前面时,先++,再赋值。
var a,b,c;
a = b = c = 42;
此处c=42的结果值为42,副作用是将c赋值42,然后b=42结果值为42,副作用将b赋值42,最后是a.

链式赋值时,必须先声明变量再赋值。

var a = b = c = 42;
//没有对b,c进行声明,会出现全局变量或者报错。

try...catch...finally

function foo() {
  try {
    console.log(42);
  } finally {
    console.log("hello");
  }
  console.log("never");
}
foo();
//42
//hello
//never
function foo() {
  try {
    return 42;
  } finally {
    console.log("hello");
  }
  console.log("never");
}
console.log(foo());
//hello
//42

异步

将一段代码包装成一个函数,并指定它在响应某个时间时执行。此时就在代码中创建了一个将来执行的块,也就是异步机制。

异步强调现在与将来的时间间隙,并行强调能够同时发生的事情。

javascript是单线程。两个函数按照调用顺序执行,但引入异步(例如ajax请求)之后,不一定先调用哪一个函数;并且还有可能同时调用。

  • 并发:两个或者多个进程同时执行,同时是指在同一段时间内,并不需要在同一时刻。

例如,进程1和进程2并发进行,但是他们的各个时间是在事件循环队列中依次运行的。

场景:更新状态列表,往下滚动而逐渐加载更多内容。
需要进程1:触发onscroll事件时做出响应(发起ajax请求)
进程2:接受ajax响应(将内容展示到页面)
onscroll,请求1    <----进程1启动
onscroll,请求2
响应1              <----进程2启动
onscroll,请求3
响应2
onscroll,请求4
onscroll,请求5
onscroll,请求6    <----进程1结束
响应3
响应4
响应6
响应5              <----进程2结束
  • 进程交互:两个或者多个进程结果的先后顺序会对运行结果产生不同的影响。

例如想将结果按顺序存储在res[]当中。

let res = [];
function response(data){
  res.push(data)
}
ajax("http://url.1",response);
ajax("http://url.2",response);
//想将url.1结果放在res[0]当中,url.2结果放在res[1]当中。
//但两个不一定谁先响应完,因此res.push无法保证url.1一定在第一个。
  • 并发协作:将一个长期运行的进程分割成多个步骤或多批任务,使得其他并发进程有机会将自己的运算插入到事件循环队列中交替运行。

回调

根据回调可以实现代码的顺序执行;

A();
ajax('',C());
B()
//回调嵌套
B(){
  C(){
    D(){
      E(){}
    }
  }
}

先执行A(),然后执行B(),等ajax拿到数据之后执行C。

但是嵌套之后,C里面有D,D里面有E,一层层包裹下去代码的中间肚子部分会很大,也叫作回调地狱,其代码可视性差。并且代码当中需要对每一个步骤做出不同情况的处理(出错了,调用次数多了,响应早了,响应晚了等等很多情况进行针对性的处理)。

回调地狱中也有问题在于控制反转,也就是回调代码中有引用第三方库,但是第三方库可能出问题,导致你这边出现问题,所以你得把所有可能出现的问题都在书写代码时给预防出来。但每个异步回调重复这样的工作最后成为了负担。回调暗中将控制权交给第三方工具,这个缺陷也叫作可信任性方面的不足。

所以就会想解决方案,比如回调时候API设计了success和failure两种处理方式。

\

promise

为了保障按顺序执行,并且有成功失败不同的解决方案,就产生了promise和事件订阅。

promise有三种不同的状态,pending,reject,resolve。第一种表明还未处理。promise.then{成功回调,失败回调}可以写入针对接受和拒绝两种方案的回调函数,这样就可以保障顺序,同时会有不同的解决方案。

事件订阅可以在事件调用时,通知一个listener事件订阅对象,调用代码时得到这个对象,并在其上注册了两个事件处理函数。如下所示。


function foo(){
  return listener;
}
let evt = foo(42);//得到listener对象
evt.on("completion",function(){
  //成功回调
})
evt.on("failure",function(){
  //失败回调
})

thenable

promise中很重要的一个细节就是如何确定某个值是不是真的promise。识别promise就是定义一种成为thenable的东西,也就是任何具有then()方法的对象和函数。

最简单的判断thenable方案就是

if(p!==null && (typeOf p === 'object'||typeOf p === 'function') &&
  typeOf p.then === 'function'){
  //假定是thenable
}else{
  //不是thenable
}

但这种有不足,比如

  1. 你使用了then完成promise,但并不想它被当做promise或者thenable
  2. 其原型链上有then,是function,但本身只是一个对象。此对象就会被误认为是thenable

promise信任问题

之前的回调问题(回调过早,回调 过晚,调用次数过少或过多,未能传递所需的环境和参数,吞掉可能出现的错误和异常)在promise可以得到解决。

  1. 调用过早

立即完成的promise不会被同步观察到,也就是说,调用then的时候,即使promise已经决议,有了reject还是resolve的结果,then当中的回调也总是会被异步调用。(此异步调用为微任务,会保证当前同步任务执行完毕的情况下才进行微任务。宏任务微任务这些概念涉及到event loop事件循环)

  1. 调用过晚

promise创建对象调用resolve或reject时,then的回调会被自动调度,并且可以保证这些调度的回调在下一个异步事件点一定会被触发。

  1. 回调未调用

将foo()与3秒钟延时promise进行promise.race操作,给他三秒钟,如果foo在3s内有反应就及时完成,否则就3秒钟promise里面报超时

错误。

  1. 调用次数过多或过少

promise决议只会有一次,之后再有reject或者resolve也不会被更改,所以任何通过then的回调也只会被调用一次。所以调用次数不会过多,也肯定会有一次。

  1. 未传参

每次都会将成功或者失败的参数传给对应的回调函数

决议、完成和拒绝

resolve(...)表明决议,为promise设定的最终状态,可以是fulfilled也可以是rejected。

fulfill(...)表明完成

reject(...)表明拒绝

错误处理

同步错误处理可以采用try...catch,异步得采用单独的catch方案。

关于期约可进一步查看红宝书当中的解说笔记 期约(promise)、异步函数(async/await)