欢迎光临
我们一直在努力

JS数据类型、变量声明的提升和函数提升

一、 类型

1.1 JavaScript 是弱类型的语言

JavaScript 是弱类型的语言,体现在:不管什么类型的变量,都要用varletconst(常量)定义。

比如:

var a = 100;
var b = "哈哈";
var c = true;
var d = undefined;

这点和 C 语言、 Java 截然不同。它们必须要带着类型来定义变量:

int a = 100;
string b = "哈哈";

1.2 JavaScript 中的类型

JavaScript 中类型分两大类:基本类型值和引用类型值

基本类型值:numberstringbooleanundefinednull

引用类型值:objectarrayfunctionregexpsymbol

JS 中检查一个字面量、变量的类型的最简单的方法是typeof关键字。typeof检测结果只有7种,特别好记,和葫芦娃的个数一样多。

类型 用typeof检测结果
number "number"
string "string"
boolean "boolean"
undefined "undefined"
null "object"
object
array
regexp
map
function "function"
symbol "symbol"

1.3 基本类型值和引用类型值的关系

基本类型值和引用类型值区别就两点:

  • ① 变量传递:引用类型值,不会在内存中克隆一份这个值,而是将引用传给新的变量;而基本类型值,会在内存中克隆一份这个变量。
  • ==或者===判断时:引用类型值,判断两个东西是不是内存中的同一个东西;而基本类型值,是判断值(及类型)的相等情况。

比如小例子:

var arr1 = [0, 1, 2, 3];
var arr2 = arr1;

arr1.push(4);
console.log(arr2); // [0, 1, 2, 3, 4]

我们说一下原因,为什么arr1push了一项,而arr2也变化了。

因为数组是典型的引用类型值,在遇见var arr2 = arr1;这样的语句的时候,内存中不会克隆 arr1 这个数组,而是直接将变量 arr2 指向同一个区域。即 arr1 和 arr2 压根就是同一个东西。

数组的

注意:在进行 == 或者 === 判断的时候,引用类型值是在判断这俩是不是同一个东西!即在内存中是不是同一个对象

面对 == 或者 === 运算符,参与比较的如果是引用类型值,那么此时要判断它们在内存中是不是同一个东西,如下:

console.log([] == []);                          // false
console.log({ a: 1 } == { a: 1 });              // false
console.log(/\d/ == /\d/);                      // false
console.log(function() {} == function() {});    // false

空数组[]字面量,和另一个空数组[]字面量不是内存中的同一个东西。

这个事情在基本类型值上不会发生,如下:

console.log(3 == 3);            // true
console.log("哈哈" == "哈哈");   // true
console.log(null == null);      // true

对于基本类型值,== 比较值是否相等,=== 比较值和类型是否都相等;但是 == 和 === 都不关心它们是不是内存中的同一个量。

注意null在这里体现出了基本类型值的特点,但是typeof null == 'object'; // true

面试题1:

var arr1 = [0, 1, 2, 3];
var arr2 = arr1;
arr1.unshift(arr2.pop());
arr2.unshift(arr1.pop());

console.log(arr1);

答案:[2, 3, 0, 1]

面试题2:

var arr1 = [0, 1, 2, 3];
var arr2 = arr1;
arr1 = [0, 1, 2, 3, 4];

console.log(arr2);

答案:[0, 1, 2, 3]

1.4 深克隆和浅克隆

只有引用类型值才能讨论深克隆和浅克隆。

不克隆:

var arr1 = [0, 1, 2, 3];
var arr2 = arr1;

根据上一小节的学习知识,我们知道这两行语句的结果是 arr1、arr2 两个变量都指向了内存中的同一个数组区域。所以,压根没有任何克隆发生。

如何克隆一个数组呢?容易想到的方法就是for遍历。

var arr1 = [1, 2, 3, 4];
var arr2 = [];

for (var i = 0; i < arr1.length; i++) {
    arr2.push(arr1[i]);
}

console.log(arr2);
console.log(arr1 == arr2);  // false

为什么说这个克隆是浅克隆呢??因为如果原数组中某一项的值,是引用类型值,这项还是没有分开。

比如这个小例子,arr1 数组中有一项仍然是数组,我们用 for 循环的方法进行遍历,会发现遍历到这个小数组的时候,计算机不会复制它。

var arr1 = [1, 2, 3, 4, ["A", "B", "C"]];
var arr2 = [];

for (var i = 0; i < arr1.length; i++) {
    arr2.push(arr1[i]);
}

console.log(arr2);
console.log(arr1 == arr2);          // false
console.log(arr1[4] == arr2[4]);    // true

浅克隆的定义:只克隆一层,不再继续深入克隆下。

如何实现深克隆呢?答:递归。

function deepClone(o) {
    // 首先检查它是不是数组
    if (Array.isArray(o)) {
        var arr = [];
        for (var i = 0; i < o.length; i++) {
            arr.push(deepClone(o[i]));
        }
        return arr;
    }
    // 然后检查是不是对象
    else if (typeof o == 'object' && target !== null) {
        var obj = {};
        for (var k in o) {
            obj[k] = deepClone(o[k]);
        }
        return obj;
    }
    // 如果是基本类型值,返回它本身
    else {
        return o;
    }
}

var arr1 = [1, 2, 3, { "a": 44, "b": 45 }, [77, 88]];
var arr2 = deepClone(arr1);

console.log(arr2);
console.log(arr1[3] == arr2[3]); // false

这里使用了递归算法,事实上面试常见的递归算法就是6个:

深克隆、函数柯理化、数组扁平化、阶乘、斐波那契数列、杨辉三角。

1.5 number 类型

number类型值:数字、NaNInfinity

1.5.1 说说 NaN

Not a number 不是一个数,但实际上它就是一个 number 类型的字面量。

console.log(typeof NaN);    // number

什么时候会出现 NaN:这是一个数学运算,但是结果就是出不来数字,其他语言就报错了,但是JS不会报错,而是给出 NaN。

console.log("我" - "你");         // NaN
console.log("我" * 2);           // NaN
console.log(parseInt("我"));     // NaN

NaN 与自己都不相等

console.log(NaN == NaN);    // false

此时判断某值是不是 NaN 就成为了一个问题。JS 提供了一个函数叫做isNaN(),但是这个函数也是个大坑,它不好用:

console.log(isNaN(NaN));        // true
console.log(isNaN("字符串"));    // true

内部机制就是隐式转换的问题。任何值如果不能被Number()转为正常数字,就会被转为 NaN,那么用isNaN()检测会返回true

1.5.2 Number() 隐式转换

字符串和数字进行乘法,结果是数字,说明字符串"3"可以被转为数字 3。

"3" * 8     // 24

这就是隐式转换。

Number()它的结果

isNaN()它的结果

33

33

false

"33"

33

false

"33岁"

NaN,字符串要想转为数字,必须是纯数字字符串

true

""

0

false

"33.4545"

33.4545

false

"3e5"

30000

false

"二百五"

NaN

true

true

1

false

false

0

false

undefined

NaN

true

null

0

false

[]

0

false

[99]

99

false

[11,22,33]

NaN

true

JS 内置了一个Number()这个函数,可以将值转换为number类型值。

1.5.3 数学运算符

+、-、*、/、%

注意%表示取余:

5 % 3   // 2
5 % 5   // 0

1.5.4 特殊值的运算结果

说一下特殊值的运算结果,有 NaN 参与的一切数学运算,结果都是 NaN

console.log(NaN * 12);
console.log(NaN * null);
console.log(NaN * undefined);
console.log(NaN * Infinity);
console.log(NaN - NaN);
console.log(Infinity + 4);          // Infinity 
console.log(Infinity + Infinity);   // Infinity 
console.log(Infinity - Infinity);   // NaN

1.6 string 类型

String 是内置的构造函数,Java 中称为“包装类”。

String()构造函数可以将任何其他值变为字符串类型,本质上就是调用了它们的toString()方法,但是一些值无法调用toString()方法,却能被String()转:

null.toString();    // 报错 Cannot read property 'toString' of null
String(null);       // "null"

undefined.toString();   // 报错
String(undefined);      // "undefined"

注意一些特殊值

[1, 2, 3, 4].toString();    // 1,2,3,4
String([1, 2, 3, 4]);       // 1,2,3,4

{}.toString();  // [object Object]
String({});     // [object Object]

注意toString()方法是Object.prototype身上的方法,对于数组来说,如果用call

Object.prototype.toString.call([1, 2, 3, 4]);   // "[object Array]"

所以检查一个东西是不是数组,可以这样检查:

Object.prototype.toString.call(arr) == "[object Array]";

1.7 boolean 类型

1.7.1 Boolean() 隐式转换

Boolean() 它的结果

它 == true 的结果

10

true

false

Infinity

true

false

0

false

false

NaN

false

false

null

false

false

undefined

false

false

""

false

false

"我"

true

false

"false"

true,字符串不是空的,就是true

false

[]

true,数组一律是true

false

[1,2,3]

true,

false

{}

true,对象一律是true

false

{"a":1}

true

false

if 语句中走的是什么机理??走的是Boolean()

if (10) {
    console.log("A");
}

if (10 == true) {
    console.log("B");
}

结果是A

!一些特殊值的结果,就是上表的第二列的结果。

console.log(![]);
console.log(![1,2,3,4]);
console.log(!"我爱你");
console.log(!"");
console.log(!0);
console.log(!undefined);
console.log(!null);

遇见这种东西,不要怕:

if(![]){
    console.log("123");
}

不能输出,因为[]数组一定是真性的,![]就是false

1.7.2 短路计算

&&且、||

  • a && b 都真才真。

    • a 如果为假,b 就会被短路,总结果就是 a。
    • a 如果为真,b 就决定了总结果,所以总结果就是 b。
  • a || b 有真才真

    • a 如果为真,b 就会被短路,总结果就是 a。
    • a 如果为假,b 就决定了总结果,所以总结果就是 b。
console.log([] && 13);          // 13
console.log("false" && 13);     // 13
console.log([] || undefined);   // []
console.log(true && false);     // false
console.log(true && "小猪仔");   // "小猪仔"

1.7.3 逻辑运算(综合运算)

逻辑运算中,运算顺序:非 → 与 → 或。

总运算顺序:非 → 数学 → 比较 → 逻辑

面试题几乎不会出综合运算题目,但是挺锻炼综合素养的。

[] + "" && false || !undefined
// 解:原式  = [] + "" && false || true
//         = "" && false || true
//         = false  || true
//         = true

二、提升

2.1 变量声明的提升

JavaScript 程序在执行之前,有一个预读阶段(预解析)。在预读阶段,程序会识别所有的变量定义。

如果使用一个变量,但是这个变量没有定义过,会报错:console.log(a); // 报错,a is not define

如果使用一个变量,这个变量先使用然后再定义,这个变量会输出undefined

console.log(a);        // undefined
var a = 10;

变量在预读阶段,提升的只是定义,不是它的值。所以JS规定,这种没有值的变量,值默认是 undefined。
好比:

var a;
console.log(a);        // undefined

需要注意的是,变量声明的提升,会把变量提升到当前作用域的最开始位置。

var a = 20;

function fun(){
    console.log(a);
    var a = 10;
}

fun();

变量声明的提升

还需要注意的是,变量声明的提升无视if语句。经典面试题:

var a = 10;
function fun(){
    if(!a){
        var a = 20;
    }
    console.log(a); // 20
}

fun();

变量声明的提升是无条件的,无视if语句。所以,if语句那里判断的是if(! undefined)当然是真。

还要注意事情,就是当作用域内有一个同名的形参的时候,变量声明的提升会比形实结合更早,所以更不占优势。

var a = 10;
function fun(a){
    if(!a){
        var a = 20;
    }
    console.log(a); // 30
}

fun(30);

在预读阶段,var a会被提升。但是在程序真正执行阶段,才进行形实结合。形实结合结合不是var,如同没有占用这个标志符。

2.2 let 和 const 的情况

letconst没有提升,并且还会形成暂时性死区(TDZ)。

console.log(a); // 报错:Cannot access 'a' before initialization,因为const定义的变量没有提升。
const a = 10;

一个函数内部,const 和 let 肯定是不提升的,但是在预读阶段如果发现了这个小作用域出现了同名的 const、let 的变量,此时会报错。这就是暂时性死区。

const a = 10;
function fun(){
    console.log(a);
    const a = 20;
}

fun();

报错:

function fun(a){
    let a = 10;
}

fun(6);

就是说,用let和const定义变量和常量的时候,不希望遇见同名变量。

2.3 函数的提升

函数都说声明头提升,实际上本质上就是整体提升。

JS允许我们先调用函数,然后再定义函数。

fun();

function fun(){
        console.log("A");
}

输出A。

下面的程序,雄辩的证明了函数是在预读取阶段整体提升的:

fun();

while(true){

}

function fun(){
    console.log("A");
}

能输出A,然后死循环。

程序的执行阶段,是不再定义函数的。

更好的例子:

var m = 10;
fun(); // 10

while(true){

}

var m = 20;

function fun(){
    console.log(m);
}

注意,用var定义的匿名函数,不整体提升。

fun(); // 报错
var fun = function(){
    console.log("A");
}

报错。因为var fun属于变量,只提升变量的定义,不是函数的定义。

函数会优先提升,所以不占优势。

比如现在有一个函数叫做fun,一个变量也叫做fun,程序执行结果如何呢??

fun(); // B

var fun = function(){
    console.log("A");
}

function fun(){
    console.log("B");
}

fun(); // A

函数的提升

面试题:

var m;

fun();
m();

function fun(){
    function m(){
        console.log("★");
    }
}

答案是:报错,不能输出★。这是因为内部的m函数已经在预读阶段整体提升了,在函数执行阶段,已经不再读取它的定义了。

②:

function t5(greet){
  var greet='hello';
  alert(greet); // hello
  function greet(){
  }
  alert(greet); // hello
}
t5(null);

③:

function a(b){
    console.log(b);
    function b(){

    }
}
a(1);

所有的形式参数,是没有var的,形参的这个b如同没有在函数内部定义。

④:

function a(b){
    console.log(b); // 1
    b = function(){
      console.log(b); // ƒ (){ console.log(b); }
    }
    b();
}
a(1);
赞(4) 打赏
未经允许不得转载:前端学习分享网 » JS数据类型、变量声明的提升和函数提升

评论 抢沙发

评论前必须登录!

 

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏