js的变量和类型

ECMAScript变量可能包含两种不同数据类型的值:基本类型值和引用类型值。基本类型值指的是简单的数据段,而引用类型值指那些可能由多个值构成的对象。

基本类型和引用类型这里可称为 原始类型和对象类型

原始类型

Null: 只包含一个值: null
Undefined: 只包含一个值:undefined
Boolean: 包含两个值: truefalse
Number:整数或浮点数,还有一些特殊值(-Infinity+InfinityNaN
String: 一串标识文本值的字符序列
Symbol: 一种实例是唯一且不可改变的数据类型(es6新增)

对象类型

Object: 除了常用的Object, ArrayFunction等都属于特殊的对象

怎么区分原始类型和对象类型

1.不可变性

上面所提到的原始类型,在ECMAScript标准中,它们被定义为 primitive values,即原始值, 代表值本身是不可被改变的。

以字符串为例, 我们在调用操作字符串的方法时,没有任何方法是可以直接改变字符串的:

var str = 'ConardLi';
str.slice(1);
str.substr(1);
str.trim(1);
str.toLowerCase(1);
str[0] = 1;
console.log(str.slice(1)); //onardLi
console.log(str.substr(1)); //onardLi
console.log(str.trim(1)); //ConardLi
console.log(str.toLowerCase(1)); //conardLi
console.log(str[0]); // C
console.log(str);  // ConardLi

在上面的代码中我们对str调用了几个方法,无一例外,这些方法都在原字符串的基础上产生了一个新字符串,而非直接去改变str, 这就印证了字符串的不可变性。

那么,当我们继续调用下面的代码:

str += 6;
console.log(str); // ConardLi6

你会发现,str的值被改变了,这不就打脸了字符串的不可变性么? 其实不然,我们从内存上来理解:

在 Javascript 中,每一个变量在内存中都需要一个空间来存储。

内存空间又被分为两种, 栈内存与堆内存。

栈内存:

  • 存储的值大小固定
  • 空间较小
  • 可以直接操作其保存的变量,运行效率高
  • 由系统自动分配存储空间

Javascript中的原始类型的值被直接存储在栈中,在变量定义时,栈就为其分配好了内存空间。


image.png

由于栈中的内存空间的大小是固定的,那么注定了存储在栈中的变量就是不可变的。

在上面的代码中,我们执行了str += 6的操作,实际上是在栈中又开辟了一块内存空间用于存储'ConardLi6' ,然后将变量 str 指向这块空间, 所以这并不违背 不可变性 的特点

image.png
2.引用类型

堆内存:

  • 存储的值大小不定,可动态调整
  • 空间较大,运行效率低
  • 无法直接操作其内部存储,使用引用地址读取
  • 通过代码进行分配空间

相对于上面具有不可变性的原始类型,我们习惯把对象称为引用类型,引用类型的值实际存储在堆内存中,它在栈中只存储了一个固定长度的地址,这个地址指向堆内存中的值。

var obj1 = {name:"ConardLi"}
var obj2 = {age:18}
var obj3 = function(){...}
var obj4 = [1,2,3,4,5,6,7,8,9]

image.png

当然,引用类型就不再具有不可变性了,我们可以轻易的改变它们:

var obj1 = {name:"ConardLi"}
var obj2 = {age:18}
var obj4 = [1,2,3,4,5,6,7,8,9]
obj1.name = "ConardLi6";
obj2.age = 19;
obj4.length = 0;
console.log(obj1); //{name:"ConardLi6"}
console.log(obj2); // {age:19}
console.log(obj4); // []

以数组为例,它的很多方法都可以改变它自身。

  • pop() 删除数组最后一个元素,如果数组为空,则不改变数组,返回undefined,改变原数组,返回被删除的元素
  • push()向数组末尾添加一个或多个元素,改变原数组,返回新数组的长度
  • shift()把数组的第一个元素删除,若空数组,不进行任何操作,返回undefined,改变原数组,返回第一个元素的值
  • unshift()向数组的开头添加一个或多个元素,改变原数组,返回新数组的长度
  • reverse()颠倒数组中元素的顺序,改变原数组,返回该数组
  • sort()对数组元素进行排序,改变原数组,返回该数组
  • splice() 从数组中添加、删除项目,改变原数组,返回被删除的元素

下面我们通过几个操作来对比一下原始类型和引用类型的区别:

3.复制

当我们把一个变量的值复制到另一个变量上时,原始类型和引用类型的表现是不一样的,先来看看原始类型:

var name = 'ConardLi';
var name2 = name;
name2 = 'code秘密花园';
console.log(name); // ConardLi;

image.png

内存中有一个变量name,值为ConardLi。我们从变量name复制出一个name2,此时在内存中创建了一块新的空间用于存储ConardLi,虽然两者值相同的,但是俩者指向的内存空间完全不同,这两个变量参与任何操作都互不影响。

复制一个引用类型:

var obj = {name:'ConardLi'};
var obj2 = obj;
obj2.name = 'code秘密花园';
console.log(obj.name); // code秘密花园
image.png

当我们复制引用类型的变量时,实际上复制的是栈中存储的地址,所以复制出来的obj2实际上和obj指向的堆中同一个对象。因此,我们改变其中任何一个变量的值,另一个变量都会受到影响,这就是为什么会有深拷贝和浅拷贝的原因。

4.比较

当我们在对两个变量进行比较时,不同类型的变量的表现是不同的:

image.png
var name = 'ConardLi';
var name2 = 'ConardLi';
console.log(name === name2); // true
var obj = {name:'ConardLi'};
var obj2 = {name:'ConardLi'};
console.log(obj === obj2); // false
console.log(obj.name == obj2.name); // true

对于原始类型,比较时会直接比较它们的值,如果值相等,即返回true
对于引用类型,比较时会比较它们的引用地址,虽然两个变量在堆中的对象具有的属性值是相等的,但是他们被存储在了不同的存储空间,因此比较值为false

这里对对象的属性的存储做个简单说明:对象的内容是由一些存储在特定命名位置的(任意类型的)值组成的,我们称之为属性
但是需要强调的一点是,当我们说’内容‘时,似乎在暗示这些值实际上被存储在对象内部,但是这
只是它的表现形式。在引擎内部,这些值的存储方式是多种多样的,一般并不会存在对象的容器内部。存储对象容器内部的是这些属性的名称,他们就像指针一样,指向这些真正的存储位置

image.png
var Person = {
  name:luozc,
  age:21,
  job:student
}

如果要访问Person中的name的位置,我们需要使用.操作符或者[]操作符。.name语法通常被称为”属性访问“,["name"]语法通常被称为”键访问“。实际上它们访问的是同一个位置,并且会返回相同的值,所以这两个术语是可以互换的。

5.值传递和引用传递

借助下面的例子,我们先来看一看什么是值传递,什么是引用传递:

let name = 'ConardLi';
function changeValue(name){
  name = 'code秘密花园';
}
changeValue(name);
console.log(name);

执行上面的代码,如果最终打印出来的是nameConardLi,没有改变,说明函数参数传递的是变量的值,是值传递。如果最终打印的是code秘密花园,函数内部的操作可以改变传入的变量,那么说明函数参数传递的是引用,即引用传递。

很明显,上面的执行结果是ConardLi,即函数参数仅仅是被传入变量复制给了的一个局部变量,改变这个局部变量不会对外部变量产生影响。

let obj = {name:'ConardLi'};
function changeValue(obj){
  obj.name = 'code秘密花园';
}
changeValue(obj);
console.log(obj.name); // code秘密花园

上面的代码可能让你产生疑惑,是不是参数是引用类型就是引用传递了呢?

首先明确一点,ECMAScript中所有的函数的参数都是按值传递的

同样的,当函数参数是引用类型时,我们同样将参数复制了一个副本到局部变量,只不过复制的这个副本是指向堆内存中的地址而已,我们在函数内部对对象的属性进行操作,实际上和外部变量指向堆内存中的值相同,但是这并不代表着引用传递,下面我们再按一个例子:

let obj = {};
function changeValue(obj){
  obj.name = 'ConardLi';
  obj = {name:'code秘密花园'};
}
changeValue(obj);
console.log(obj.name); // ConardLi

可见,函数参数传递的并不是变量的引用,而是变量拷贝的副本,当变量是原始类型时,这个副本就是值本身,当变量是引用类型时,这个副本是指向堆内存的地址。所以,再次记住:

ECMAScript中所有的函数的参数都是按值传递的。

引申深浅拷贝

举个栗子:
slice(0)和深拷贝有什么区别呢

var obj = [
    {
        name:'melin1',
        job:'111'
    },
    {
        name:'melin2',
        job:'222'
    },
    {
        name:'melin3',
        job:'333'
    }
];
var copy = obj.slice(0);
copy[1].name = 'tom';
console.log(obj[1].name); //tom
console.log(copy[1].name); //tom

结果是obj[1].name和copy[1].name都被修改了。slice可看作浅拷贝,因为如果obj有引用类型的元素,slice仅仅是复制了元素的地址。

  • (1)拷贝是指得到被拷贝对象的副本,副本的修改不会影响到原对象;
  • (2)js的传参是按值传递,但是对于引用类型,传递的值是原对象在内存中的地址,所以拷贝仅仅是获取了原对象的引用;
  • (3)在 (2) 的基础上,对拷贝进行修改,原对象也会被修改;
  • (4)要想避免(3)的情况出现,就不能仅仅拷贝地址,而是要将原对象的属性树遍历复制到拷贝上,这样拷贝和原对象就是完全独立的了;
  • (5)(4)的情况叫深拷贝,与之相对, (2) 的情况叫浅拷贝;
  • (6)如果obj所有值都是非引用类型,那么obj.slice(0)与深浅拷贝没有差别;
  • (7)如果obj有引用类型的元素的话,obj.slice(0)仅仅是复制了元素的地址,,obj.slice(0)可看作浅拷贝。
基本包装类型(特殊的引用类型:Boolean、Number、String)
  • 为了便于操作基本类型值,ECMAScript还提供了3个特殊的引用类型:Boolean、Number和String。 这些类型与其他引用类型相似,但同时也具有与各自的基本类型相应的特殊行为。
var s1 = "some text";
var s2 = s1.substring(2);

我们知道,基本类型值不是对象,因而从逻辑上讲它们不应该又方法。其实,为了让我们实现这种直观的操作,后台已经自动完成了一系列的处理。 当第二行代码访问S1时,访问过程处于一种读取模式,也就是要从内存中读取直观字符串的值。而在读取模式中访问字符串时,后台都会自动完成下列处理。

  • (1)创建String类型的实例;
  • (2)在实例上调用指定的方法;
  • (3)销毁这个实例。
    可以将以上三个步骤想象成是执行了下列ECMAScript代码。
var s1 = new String("some text");
var s2= s1,subString(2);
s1 = null;

经过此番处理,基本的字符串值就变得跟对象一样了。而且,上面这三个步骤也分别适用于Boolean和Number类型对应的布尔值和数字值。

引用类型与基本包装类型的主要区别就是对象的生存期。使用new操作符创建的引用类型的实例在执行流离开当前作用域之前都一直保存在内存中。而自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,然后立即被销毁。这意味着我们不能在运行时为基本类型值添加属性和方法。

本文参考:https://juejin.im/post/5cec1bcff265da1b8f1aa08f

面试题:
题目一:

var obj = [
    {
        name:'melin1',
        job:'111'
    },
    {
        name:'melin2',
        job:'222'
    },
    {
        name:'melin3',
        job:'333'
    }
];
var copy = obj.slice(0);
copy[1].name = 'tom';
console.log(obj[1].name); //tom
console.log(copy[1].name); //tom

题目二:

let obj = {name:'ConardLi'};
function changeValue(obj){
  obj.name = 'code秘密花园';
}
changeValue(obj);
console.log(obj.name); // code秘密花园


let obj = {};
function changeValue(obj){
  obj.name = 'ConardLi';
  obj = {name:'code秘密花园'};
}
changeValue(obj);
console.log(obj.name); // ConardLi
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容