阮一峰的JavaScript 教程读书笔记之数据类型

本文详细介绍了JavaScript的基础知识,包括数据类型(数值、字符串、布尔值、undefined、null和对象)、对象操作(属性读取与更改、属性的in运算、for...in语句和with语句)以及函数的声明、作用域、属性和方法。重点讨论了函数的闭包概念,以及立即调用函数式和数组的相关操作。通过实例解析了JavaScript中的一些常见陷阱和最佳实践。
摘要由CSDN通过智能技术生成


这个博客就想记录一些读书笔记, 阮一峰的JavaScript 教程

数据类型概述

JS中具有六种数据类型。

  • 数值(number):整数和小数
  • 字符串(string):文本
  • 布尔值(boolean):包括true和false
  • undefined:表示“未定义“或不存在
  • null:空值,表示此处的值为空
  • 对象(object):各种值组成的集合
    而对象又包括:
    (1)狭义的对象(object)
    (2)数组(array)
    (3)函数(function)

数值记录

parseInt函数会获取字符串的数值,但是当遇到第一个不是数值的就会停止寻找。

  parseInt('1.1');//1
  parseInt('123');//123

而在java中就会报错

Integer.parseInt("1.23")//java.lang.NumberFormatException: For input string: "1.23"

如果根本就不是数值的话就会返回NaN,但是js有个特例就是如果有正负号是可以的,同时如果是满足进制也可以转换。

parseInt('abc') // NaN
parseInt('.3') // NaN
parseInt('') // NaN
parseInt('+') // NaN
parseInt('+1') // 1
parseInt('0x10') // 16

parseInt类似的还有parseFloat,以及isNaN()判断一个值是否为NaN
但是,isNaN只对数值有效,如果传入其他值,会被先转成数值。比如,传入字符串的时候,字符串会被先转成NaN,所以最后返回true,这一点要特别引起注意。也就是说,isNaN为true的值,有可能不是NaN,而是一个字符串。
当判断一个数值是否为NaN一般利用它自身不等于自身的特性

function myIsNaN(value) {
  return value !== value;
}

isNaN类似的还有isFinite方法,该方法返回一个布尔值,表示某个值是否为正常的数值。除了Infinity、-InfinityNaNundefined这几个值会返回false,isFinite对于其他的数值都会返回true。

isFinite(Infinity) // false
isFinite(-Infinity) // false
isFinite(NaN) // false
isFinite(undefined) // false
isFinite(null) // true
isFinite(-1) // true

字符串

在JavaScript中字符串可以看成字符数组,这里说的可以看成是可以直接根据下标来拿字符,类似于java的charAt

var s = 'hello';
s[0] // "h"
s[1] // "e"
s[4] // "o"

// 直接对字符串使用方括号运算符
'hello'[1] // "e"

但不能更改字符串的字符,同时也不能更改长度(length属性)

var s = 'hello';

s[1] = 'a';
s // "hello"

s.length = 3;
s.length // 5

对象

对象就是一组“键值对”(key-value)的集合,是一种无序的复合数据集合,对象的属性,用分开。

var obj = {
  foo: 'Hello',
  bar: 'World'
};

注意:键名(属性)要求是字符串,所以加不加''都可以,但键名要合理

// 报错
var obj = {
  1p: 'Hello World'
};

键值可以是任何数据类型,比如说是对象中的函数,那么这个属性就叫做方法

var obj = {
  p: function (x) {
    return 2 * x;
  }
};

obj.p(1) // 2

如果属性的值还是一个对象,就形成了链式引用。注意这里相当于动态创造了o1的属性。

var o1 = {};
var o2 = { bar: 'hello' };

o1.foo = o2;
o1.foo.bar // "hello"

如果不同的变量名,指向同一个对象,那么相当于指向了同一块内存地址。修改其中一个变量,会影响到其他所有变量。

var o1 = {};
var o2 = o1;

o1.a = 1;
o2.a // 1

o2.b = 2;
o1.b // 2

此时如果更改变量的引用,就不会更改其他变量。下面相当于o1指向了1,而o2还是指向原先的内存地址。

var o1 = {};
var o2 = o1;

o1 = 1;
o2 // {}

但是,这种引用只局限于对象,如果两个变量指向同一个原始类型的值。那么,变量这时都是值的拷贝

var x = 1;
var y = x;

x = 2;
y // 1
属性的操作
属性的读取与更改

属性值的读取可以使用点运算符和方括号运算符,但这里要切记,方括号运算符里要用'',否则就会当成变量而非字符串。

var obj = {
  p: 'Hello World'
};

obj.p // "Hello World"
obj['p'] // "Hello World"

这就是没有在方括号运算符中加引号的后果,就会把foo当做变量,而非字符串。

var foo = 'bar';

var obj = {
  foo: 1,
  bar: 2
};

obj.foo  // 1
obj[foo]  // 2

点运算符和方括号运算符同时可以赋值,JS中允许后赋值。

var obj = { p: 1 };

// 等价于

var obj = {};
obj.p = 1;

同时,也可以使用Object.keys(),查看一个对象本身的所有属性。有点类似于java HashMap中获取所有key的方法。

var obj = {
  key1: 1,
  key2: 2
};

Object.keys(obj);
// ['key1', 'key2']

使用delete语句可以删除属性,删除成功会返回true。

var obj = { p: 1 };
Object.keys(obj) // ["p"]

delete obj.p // true
obj.p // undefined
Object.keys(obj) // []

假如删除的是一个不存在的属性,仍然会返回true。

var obj = {};
delete obj.p // true

只有当该属性是不可以被删除的时候,delete语句才会返回false

var obj = Object.defineProperty({}, 'p', {
  value: 123,
  configurable: false
});

obj.p // 123
delete obj.p // false

另外,需要注意的是,delete命令只能删除对象本身的属性,无法删除继承的属性。

in运算符

JS中可以使用in运算符判断一个属性(键)是否在对象中,但缺点是无法判断判断是自己的还是继承的,
这里toString就是继承的。

var obj = { p: 1 };
'p' in obj // true
'toString' in obj // true

应对上述问题可以使用hasOwnProperty方法判断一下,是否为对象自身的属性。

var obj = {};
if ('toString' in obj) {
  console.log(obj.hasOwnProperty('toString')) // false
}
for in语句

for in语句可以遍历对象中的所有属性。

var obj = {a: 1, b: 2, c: 3};

for (var i in obj) {
  console.log('键名:', i);
  console.log('键值:', obj[i]);
}
// 键名: a
// 键值: 1
// 键名: b
// 键值: 2
// 键名: c
// 键值: 3

for ... in语句有两个注意点
1.它遍历的属性是可遍历的,非可遍历的不会遍历(比如对象继承的toString方法)
2.它遍历的属性也包括继承的。
应对上面两个特点,我们在遍历对象的属性时,通常会与hasOwnProperty组合使用,以遍历对象自己的属性。

var person = { name: '老张' };

for (var key in person) {
  if (person.hasOwnProperty(key)) {
    console.log(key);
  }
}
// name
with语句

with 语句的格式如下,with可以对对象中的属性做统一操作,但要求语句中操作的属性必须是提前存在的,否则会创造一个当前作用域的全局变量。

with (对象) {
  语句;
}

实例一

// 例一
var obj = {
  p1: 1,
  p2: 2,
};
with (obj) {
  p1 = 4;
  p2 = 5;
}
// 等同于
obj.p1 = 4;
obj.p2 = 5;

实例二
相当于创建了一个全局变量p1。

var obj = {};
with (obj) {
  p1 = 4;
  p2 = 5;
}

obj.p1 // undefined
p1 // 4

函数

函数概述
函数的声明

(1)function
function 命令声明的代码块就是一个函数。包括函数名,参数。

function print(s) {
  console.log(s);
}

(2)函数表达式
这种写法将一个匿名函数赋值给变量。这时,这个匿名函数又称函数表达式(Function Expression),因为赋值语句的等号右侧只能放表达式。

var print = function(s) {
  console.log(s);
};

切记在这种情况下 function命令通常不加函数名,如果使用了只会在函数体内部生效,而不能在外部调用。

var print = function x(){
  console.log(typeof x);
};

x
// ReferenceError: x is not defined

print()
// function

当然有些时候也会有函数名,比如要调用自身,或进行调试(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)。

var f = function f() {};
1.2 函数的重复声明

新的声明会覆盖掉前面的声明,同时不管在哪里调用旧声明,都会使用新声明的结果,这是由于函数名的提升。

function f() {
  console.log(1);
}
f() // 2

function f() {
  console.log(2);
}
f() // 2
1.3 一等公民

JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处。

function add(x, y) {
  return x + y;
}

// 将函数赋值给一个变量
var operator = add;

// 将函数作为参数和返回值
function a(op){
  return op;
}
a(add)(1, 1)
// 2
1.4 函数名的提升

JavaScript 引擎将函数名视同变量名,所以采用function命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。所以,下面的代码不会报错。

f();

function f() {}
//正确

但是,如果采用赋值语句定义函数,JavaScript 就会报错。

f();
var f = function (){};
// TypeError: undefined is not a function
//相当于这个形式
var f;
f();
f = function () {};

注意,如果像下面例子那样,采用function命令和var赋值语句声明同一个函数,由于存在函数提升,最后会采用var赋值语句的定义。

var f = function () {
  console.log('1');
}

function f() {
  console.log('2');
}

f() // 1
2.函数的属性和方法
2.1 name属性

函数的name属性返回函数的名字,无论是function语句还是表达式形式,但假如表达式形式下还有函数名的话就会返回函数名。

function f1() {}
f1.name // "f1"
var f2 = function () {};
f2.name // "f2"
var f3 = function myName() {};
f3.name // 'myName'

name属性可以用来获取参数函数的名字。

var myFunc = function () {};

function test(f) {
  console.log(f.name);
}

test(myFunc) // myFunc
2.2 length方法

获取函数预期的参数个数,不管调用f时输入了多少个参数,length属性始终等于2。

function f(a, b) {}
f.length // 2
3.函数作用域

作用域(scope)指的是变量存在的范围。在 ES5 的规范中,JavaScript 只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。

简单来说可以理解为 在函数外定义的为全局作用域变量,在函数内定义的为函数作用域变量。注意,对于var命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量。

x变量相当于在判断语句中定义,就是一个全局变量。

if (true) {
  var x = 5;
}
console.log(x);  // 5

函数内部作用域也存在变量提升var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。

function foo(x) {
  if (x > 100) {
    var tmp = x - 100;
  }
}

// 等同于
function foo(x) {
  var tmp;
  if (x > 100) {
    tmp = x - 100;
  };
}
3.1 函数本身的作用域

上面代码中,函数x是在函数f的外部声明的,所以它的作用域绑定外层,内部变量a不会到函数f体内取值,所以输出1,而不是2。

总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。

很容易犯错的一点是,如果函数A调用函数B,却没考虑到函数B不会引用函数A的内部变量。

var a = 1;
var x = function () {
  console.log(a);
};

function f() {
  var a = 2;
  x();
}

f() // 1

同样的,函数体内部声明的函数,作用域绑定函数体内部。

function foo() {
  var x = 1;
  function bar() {
    console.log(x);
  }
  return bar;
}

var x = 2;
var f = foo();
f() // 1

上面代码中,函数foo内部声明了一个函数barbar的作用域绑定foo。当我们在foo外部取出bar执行时,变量x指向的是foo内部的x,而不是foo外部的x。正是这种机制,构成了下文要讲解的“闭包”现象。

参数

在JS中允许参数省略,可以在使用f函数时传入1个或多个或不传入参数,都不会报错。

function f(a, b) {
  return a;
}

f(1, 2, 3) // 1
f(1) // 1
f() // undefined

f.length // 2

注意,根据传递值的不同,会分成以下两种情况。
(1)原始类型值(数值,字符串,布尔型),这种值相当于传值传递,也就是说在函数体内修改参数值,不会影响到函数外部。

这相当于传递进去了值的拷贝,无论怎么修改都不会影响初始值。

var p = 2;

function f(p) {
  p = 3;
}
f(p);

p // 2

(2)复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。**也就是说传递进去的是地址,**在函数内部修改参数,就会影响到原始值。

var obj = { p: 1 };

function f(o) {
  o.p = 2;
}
f(obj);

obj.p // 2

注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。因为相当于给o进行了新的赋值,也就是o指向了别的地址,那么原地址的obj就不会发生改变了。

var obj = [1, 2, 3];

function f(o) {
  o = [2, 3, 4];
}
f(obj);

obj // [1, 2, 3]
arguments对象

arguments对象包含了函数运行时的所有参数arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。不过切记arguments是对象而不是数组。

我们可以通过length方法,判断输入了多少个参数。

function f() {
  return arguments.length;
}

f(1, 2, 3) // 3
f(1) // 1
f() // 0
函数的其他知识点
闭包

JS具有链式作用域,子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

f2函数可以使用n,但f1则不可以使用f2中的变量。

function f1() {
  var n = 999;
  function f2() {
  console.log(n); // 999
  }
}

通常我们不可以获取到函数中的局部变量,但我们可以将f2函数返回,就可以读取到函数中的局部变量。

function f1() {
  var n = 999;
  function f2() {
    console.log(n);
  }
  return f2;
}

var result = f1();
result(); // 999

而闭包就是f2函数,可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1,所以从f2可以得到f1的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

闭包有两个主要作用:一个是可以读取外层函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。

通过闭包,start的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc使得函数createIncrementor的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。

function createIncrementor(start) {
  return function () {
    return start++;
  };
}

var inc = createIncrementor(5);

inc() // 5
inc() // 6
inc() // 7

闭包为什么可以保存变量呢,可以理解为闭包(inc)一直在使用start变量,导致垃圾回收算法不能回收createIncrementor。因此这个函数提供的运行状态也不会被清除,它的内部变量就始终保存着当前值,供闭包读取。

闭包的另一个用处,是封装对象的私有属性和私有方法。

function Person(name) {
  var _age;
  function setAge(n) {
    _age = n;
  }
  function getAge() {
    return _age;
  }

  return {
    name: name,
    getAge: getAge,
    setAge: setAge
  };
}

var p1 = Person('张三');
p1.setAge(25);
p1.getAge() // 25

注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。

立即调用函数式

有时,我们需要在定义函数之后,立即调用该函数。这时,你不能在函数的定义之后加上圆括号,这会产生语法错误。

function(){ /* code */ }();
// SyntaxError: Unexpected token (

function当做语句时不能直接在后面加上圆括号,而当做表达式时可以。JavaScript规定,如果function关键字出现在行首,一律解释成语句。

数组

数组可以先定义再赋值,也不限定大小,也不限定其中的数据类型

var arr = [
  {a: 1},
  [1, 2, 3],
  function() {return true;}
];

arr[0] // Object {a: 1}
arr[1] // [1, 2, 3]
arr[2] // function (){return true;}

数组的本质是对象,数组的特殊性体现在,它的键名是按次序排列的一组整数(0,1,2…)

var arr = ['a', 'b', 'c'];

Object.keys(arr)
// ["0", "1", "2"]

只要是数组,就一定有length属性。该属性是一个动态的值,等于键名中的最大整数加上1。同样说明了数组是动态的,不需要连续的值。

var arr = ['a', 'b'];
arr.length // 2

arr[2] = 'c';
arr.length // 3

arr[9] = 'd';
arr.length // 10

arr[1000] = 'e';
arr.length // 1001

length属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员数量会自动减少到length设置的值。如果设置length属性的值大于当前元素的个数,则数组的成员数量会增加到这个值,新增的位置都是空位。

var arr = [ 'a', 'b', 'c' ];
arr.length // 3

arr.length = 2;
arr // ["a", "b"]

var a = ['a'];

a.length = 3;
a[1] // undefined

由于数组是对象,所以可以给数组设置键名(非整数的),但不会更改length的值。

var a = [];

a['p'] = 'abc';
a.length // 0

in运算符用于判断某个键名是否在对象中,同样可以应用在数组中,因为数组也是对象。

var arr = [ 'a', 'b', 'c' ];
2 in arr  // true
'2' in arr // true
4 in arr // false

上面代码表明,数组存在键名为2的键。由于键名都是字符串,所以数值2会自动转成字符串。

同理,for in表达式也是可以在数组中使用的遍历整个数组,不过要注意的是,会遍历所有键名,而非只有数字键名

var a = [1, 2, 3];
a.foo = true;

for (var key in a) {
  console.log(key);
}
// 0
// 1
// 2
// foo

因此不建议使用for in,而建议使用常规的forwhile

var a = [1, 2, 3];

// for循环
for(var i = 0; i < a.length; i++) {
  console.log(a[i]);
}

// while循环
var i = 0;
while (i < a.length) {
  console.log(a[i]);
  i++;
}

var l = a.length;
while (l--) {
  console.log(a[l]);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值