让人疑惑的代码
- 先看下面这两段代码:
JavaScriptJavaScript
function foo(){ var a = 1 var b = a a = 2 console.log(a) console.log(b) } foo()
function foo(){ var a = { name: "张三" } var b = a a.name = "李四" console.log(a) console.log(b) } foo()
- 输出的结果是什么?
- 执行第一段代码,打印出来 a 的值是 2,b 的值是 1;
- 执行第二段代码,仅仅改变了 a 中 name 的属性值,但是最终 a 和 b 打印出来的值都是 { name: “李四” };
JavaScript 是什么类型的语言
-
每种编程语言都具有内建的数据类型,但它们的数据类型常有不同之处,使用方式也很不一样,比如 C 语言在定义变量之前,就需要确定变量的类型,可以看下面这段 C 代码:
int main() { int a = 1; char* b = " 极客时间 "; bool c = true; c = a; return 0; }
-
上述代码声明变量的特点是:在声明变量之前需要先定义变量类型;把这种在使用之前就需要确认其变量数据类型的称为静态语言;
-
相反地,把在运行过程中需要检查数据类型的语言称为动态语言;比如 JavaScript 就是动态语言,因为在声明变量之前并不需要确认其数据类型;
-
前面代码中,把 int 型的变量 a 赋值给了 bool 型的变量 c,这段代码也是可以编译执行的,因为在赋值过程中,C 编译器会把 int 型的变量悄悄转换为 bool 型的变量,通常把这种偷偷转换的操作称为隐式类型转换;而支持隐式类型转换的语言称为弱类型语言,不支持隐式类型转换的语言称为强类型语言;在这点上,C 和 JavaScript 都是弱类型语言;
-
对于各种语言的类型,可以参考下图:
JavaScript 数据类型
-
JavaScript 是一种弱类型的、动态的语言;
- 弱类型,意味着不需要告诉 JavaScript 引擎这个或那个变量是什么数据类型,JavaScript 引擎在运行代码的时候自己会计算出来;
- 动态,意味着可以使用同一个变量保存不同类型的数据;
-
JavaScript 中的数据类型一种有 8 种:
类型 数据类型 描述 Boolean 原始类型 只有 true/false 两个值 Null 原始类型 只有一个值 null,typeof 检测 Null 类型时,返回的是 Object (是个 bug,一直没修改是为了兼容老的代码) Undefined 原始类型 一个没有被赋值的变量会有个默认值 undefined,变量提升的时候也是 undefined Number 原始类型 · 安全整数范围:±(253 - 1) 即 ±9,007,199,254,740,991
· 特殊值:Infinity, -Infinity, NaN (Not a Number)Bigint 原始类型 · 可以表示任意大的整数,通过在数字后加 n 或调用 BigInt() 构造函数创建
· 不能与 Number 混合运算 (需要显式转换),不支持 Math 对象中的方法String 原始类型 用于表示文本数据 Symbol 原始类型 符号类型是唯一的并且不可修改的,通常用来作为 Object 的 key Object 引用数据类型 包含了 字面量对象、数组、正则表达式、日期对象、Math、实例对象、函数…
检测有效数字使用 isNaN
-
NaN 不是一个有效数字,但属于 number 数字类型,NaN 和谁都不相等;其他数据类型转换成数字类型,不能转换就是 NaN;
-
示例代码:
let a = Number("111"); let b = Number("qwe"); console.log(isNaN(a)); // false console.log(isNaN(b)); // true console.log(isNaN("A")); // true
类型转化
-
由于自动转换具有不确定性,建议在预期为布尔值、数值、字符串的地方,全部使用 Boolean()、Number() 和 String() 函数进行显式转换;遇到以下三种情况时,JavaScript 会自动转换数据类型,即转换是自动完成的,用户不可见:
- 第一种情况,不同类型的数据互相运算;
- 第二种情况,对非布尔值类型的数据求布尔值;
- 转化为 false 的值:false、undefined、null、‘’、0、NaN;
- 转化为 true 的值:除了转化为 false 的值;
- 第三种情况,对非数值类型的值使用一元运算符(即
+
和-
);
-
对象的转化(求对象的原始值)会先执行 valueOf() ,若 valueOf() 返回的值还是对象则会执行 toString();
// eg1: let obj1 = { valueOf() { return 100; }, toString() { return 200; }, }; console.log(true + obj1); // 101 // eg2: let obj2 = { valueOf() { return {}; }, toString() { return 200; }, }; console.log(true + obj2); // 201
堆栈内存
-
在 JavaScript 的执行过程中, 主要有三种类型内存空间,分别是代码空间、栈空间和堆空间;
-
其中的代码空间主要是存储可执行代码的 (后面介绍),这里主要来说说栈空间 (调用栈,是用来存储执行上下文) 和堆空间,先看下面这段代码:
function foo(){ var a = "hello world" var b = a var c = {name:"hello world"} var d = c } foo()
-
当执行一段代码时,需要先编译,并创建执行上下文,然后再按照顺序执行代码;当执行到第 3 行代码时,其调用栈的状态如下:
-
接下来继续执行第 4 行代码,由于 JavaScript 引擎判断右边的值是一个引用类型,这时候处理的情况就不一样了,JavaScript 引擎并不是直接将该对象存放到变量环境中,而是将它分配到堆空间里面,分配后该对象会有一个在 “堆” 中的地址,然后再将该数据的地址写进 c 的变量值,最终分配好内存的示意图如下所示:
- 从上图可以清晰地观察到,对象类型是存放在堆空间的,在栈空间中只是保留了对象的引用地址,当 JavaScript 需要访问该数据的时候,是通过栈中的引用地址来访问的;
- 为什么一定要分 “堆” 和 “栈” 两个存储空间呢?所有数据直接存放在 “栈” 中不就可以了吗?
- 答案是不可以的:这是因为 JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率;比如文中的 foo 函数执行结束了,JavaScript 引擎需要离开当前的执行上下文,只需要将指针下移到上个执行上下文的地址就可以了,foo 函数执行上下文栈区空间全部回收,具体过程可以参考下图:
- 所以通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据;而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间;
-
最后一步将变量 c 赋值给变量 d 是怎么执行的:在 JavaScript 中,赋值操作和其他语言有很大的不同,原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址,所以 d = c 的操作就是把 c 的引用地址赋值给 d,参考下图:
- 从图中可以看到,变量 c 和变量 d 都指向了同一个堆中的对象;
- 所以这就很好地解释了文章开头的那个问题,通过 c 修改 name 的值,变量 d 的值也跟着改变,归根结底它们是同一个对象;
面试题
第一题:(阿里)
let a = { n: 10 };
let b = a;
b.m = b = { n: 20 }; // 多个 '=' 的操作,从右向左赋值
console.log(a);
console.log(b);
第二题:(360)
函数执行之前的准备工作
- 初始化实参集合
- 创建形参变量并赋值
- 代码执行
let x = [12, 23];
function fn(y) {
y[0] = 100;
y = [100];
y[1] = 200;
console.log(y);
}
fn(x);
console.log(x);
第三题:
var x = 10;
~ function (x) {
console.log(x);
x = x || 20 && 30 || 40;
console.log(x);
}();
console.log(x);
第四题:
let x = [1, 2], y = [3, 4];
~ function (x) {
x.push('A');
x = x.slice(0);
x.push('B');
x = y;
x.push('C');
// [3, 4, "C"] [3, 4, "C"]
console.log(x, y);
}(x);
// [1, 2, "A"] [3, 4, "C"]
console.log(x, y);
object 的 key 值
只能是基本类型值,最后会转成字符串;
会把 引用类型值 转换成 字符串 [object Object];
var a = {}, b = '123', c = 123;
a[b] = 'b';
a[c] = 'c';
// a['123'] 等价 a[123],则 a[b]、a[c] 等于 c
console.log(a[b]); // c
let a = { x: 100 };
let b = { y: 200 };
let obj = {};
obj[a] = 'aaa'; //=> { '[object Object]': 'aaa' }
obj[b] = 'bbb'; //=> { '[object Object]': 'bbb' }
console.log(obj); //=> { '[object Object]': 'bbb' }
object 的 key 值为 Symbol
var a = {}, b = Symbol('123'), c = Symbol('123')
a[b] = 'b';
a[c] = 'c';
// Symbol 是 ES6 中新增的数据类型,创建的值为唯一值,b 不等于 c
console.log(a[b]); // b
值和引用
// 第一题
var foo = {
n: 0,
k: {
n: 0,
},
};
var bar = foo.k;
bar.n++;
bar = {
n: 10,
};
bar = foo;
bar.n++;
bar = foo.n;
bar++;
console.log(foo.n, foo.k.n);
// 第二题
var foo = {
n: 1,
};
var arr = [foo];
function method1(arr) {
var bar = arr[0];
arr.push(bar);
bar.n++;
arr = [bar];
arr.push(bar);
arr[1].n++;
}
function method2(foo) {
foo.n++;
}
function method3(n) {
n++;
}
method1(arr);
method2(foo);
method3(foo.n);
console.log(foo.n, arr.length);
// 第三题
var foo = { bar: 1 };
var arr1 = [1, 2, foo];
var arr2 = arr1.slice(1);
arr2[0]++;
arr2[1].bar++;
foo.bar++;
arr1[2].bar++;
console.log(arr1[1] === arr2[0]);
console.log(arr1[2] === arr2[1]);
console.log(foo.bar);
0.1 + 0.2 是否等于 0.3
console.log(parseInt('1010', 2)); // 10
console.log((10).toString(2)); // 1010
/*
10进制 => 2进制 技巧
- 整数位:当前位的值 *2^(n-1)
- 小数位:把当前的不停乘 2 取整
0.1 转成 2机制
0.1 * 2 = 0.2 无整数 0.0
0.2 * 2 = 0.4 无整数 0.00
0.4 * 2 = 0.8 无整数 0.000
0.8 * 2 = 1.6 无整数 0.0001
0.6 * 2 = 1.2 无整数 0.00011
0.2 * 2 = 0.4 无整数 0.000110
...... 无限循环小数
*/
console.log((0.1).toString(2));
// 0.0001100110011001100110011001100110011001100110011001101......
console.log((0.2).toString(2));
// 0.001100110011001100110011001100110011001100110011001101......
console.log((0.3).toString(2));
// 0.0100110011001100110011001100110011001100110011001101 转成十进制为 0.30000000000000004
a 等于什么值会让「a == 1 && a == 2 && a == 3 」条件成立;
// 方式一:数值和对象比较,先执行 valueOf,如果还是对象则再执行 toString
var a = {
i: 1,
toString() {
return a.i++;
}
};
if (a == 1 && a == 2 && a == 3) {
console.log('OK');
}
// 方式二:数值和对象比较,先执行 valueOf,如果还是对象则再执行 toString
var a = {
i: 1,
valueOf() {
return a.i++;
}
};
if (a == 1 && a == 2 && a == 3) {
console.log('OK');
}
// 方式三:
var a = [1, 2, 3];
a.toString = a.shift;
if (a == 1 && a == 2 && a == 3) {
console.log('OK');
}
// 方式四:
var a = [1, 2, 3];
a.valueOf = a.shift;
if (a == 1 && a == 2 && a == 3) {
console.log('OK');
}
// 方式五:
var i = 0;
Object.defineProperty(window, 'a', {
get() {
// 获取 window.a 的时候触发
return ++i;
},
set() {
// 给 window.a 设置属性值的时候触发
}
});
if (a == 1 && a == 2 && a == 3) {
console.log('OK');
}
[] == ![] 的结果是 true 吗
console.log([] == ![]); // true
// 1. 单目运算符优先级更高,![] 是一个表达式,则 ![] => fasle
// 2. [] 要转成原始值,先执行 valueOf,若返回值为对象继续执行 toString
// [].valueOf() = [],[] == false
// [].toString() = '','' == false
// 3. '' == false
// Number('') => 0
// false => 0
// 4. 0 == 0 => true
正则表达式👉 案例
上一篇