# 2、数据类型概述(二)



# 1、对象

# 一、 概述

# 1.1 生成方法

对象是 JavaScript 语言的核心概念,也是最重要的数据类型。

对象是一组键值对(key-value)的集合,是一种无序的复合数据集合。 键名+键值

var obj = {
foo:'hello',
bar:'World'
}

var obj = new Object({foo:'hello',bar:'World'})

# 1.2 键名

  • 对象的所有键名都是字符串(ES6 又引入了 Symbol 值也可以作为键名),所以加不加引号都可以。上面的代码也可以写成下面这样。
var obj = {
  undefined:"und",
  null:"nu",
  123:'123123',
  true:'true',
  "m":"mmm",
  1.2:'1.2',
  "1p":"pp"
}
以上都是合法的,简单数据类型会转化成字符串
  • 嵌套的属性可以进行链式调用
var a = {
  b:{
    c:{foo:'hello'}
  }
}
a.b.c.foo // 'hello'
  • 属性可以动态创建,不必在对象声明时就指定。
var aa = {}
aa.foo = 'hello'
a.foo. // 'hello'
  • 键值可以是任意数据类型,如果属性值是一个函数,通常把这个属性称为‘方法’,可以像函数那样调用
var a = {
  p: function(x) {
    return x * 2;
  }
};
a.p(1); // 2

# 1.3 对象的引用

如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是说指向同一个内存地址。修改其中一个变量,会影响到其他所有变量。

var obj1 = { foo: "hello" };
var obj2 = obj1;
obj2.foo = "hi";
obj1; // {foo:'hi'}

如果把第一个对象赋值为简单数据类型(开辟了新的空间),不会对第二个产生影响

var obj1 = { f00: "hello" };
var obj2 = obj1;
obj2.o = "hi";
obj1 = true || 1;
obj2; // {foo:'hello',o:'hi'}

var y = 1;
var x = y;
y = 2;
x; // 1

# 二、属性的操作

# 2.1 属性的读取

两种方式,一种是使用点运算符,还有一种是使用方括号运算符。

var obj = {
  p: "Hello World"
};
obj.p; // 'Hello World'
obj["p"]; // 'Hello World'

方括号内部可以用表达式

obj["hello" + " world"];
obj[3 + 3];

数值可以不加引号,会自动转化成字符串形式

var obj = {
  123:'hello world'
};

obj.123. // 报错
obj[123] // 'hello world'

# 2.2 属性的赋值

点运算符和方括号运算符,不仅可以用来读取值,还可以用来赋值。JavaScript 允许属性的“后绑定”,可以在任意时刻新增属性,不用提前定义

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

obj.foo = 'Hello';
obj['bar'] = 'World'

# 2.3 属性的查看

查看一个对象本身的所有属性,可以使用Object.keys方法。

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

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

# 2.4 属性的删除: delete 命令

delete命令用于删除对象的属性,删除成功后返回true

var obj = {
  p: 1
}
Object.keys(obj) // ['p']
detete obj.p.    // true
Object.keys(obj) // []

对象删除已有或没有的 key 都会返回 true,所以不能根据 delete 判断是不是有这个属性,可以根据 obj.hasOwnProperty("key")判断

但是如果属性被保护,不能删除则返回 false

delete 不能删除继承属性:如 obj 的 toString 属性

var obj = Object.defineProperty({}, "p", {
  value: 123,
  configurable: false
});
obj.p; // 123
delete obj.p; // false
obj.hasOwnProperty("p"); // true

# 2.5 判断属性是否存在:in 运算符 和 hasOwnProperty

in运算符用于检查对象是否包含某个属性(注意,检查的是键名,不是键值),如果包含就返回true,否则返回false。它的左边是一个字符串,表示属性名,右边是一个对象。

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

in 运算符只能判断属性不是不是自身的,不能判断是不是继承而来,

hasOwnProperty 可以判断

var obj = {};
if ("toString" in obj) {
  console.log(obj.hasOwnProperty("toString")); // false
}

# 2.6 属性的遍历: for... in 循环

for...in循环用来遍历一个对象的全部属性。

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

for (var key in obj) {
  console.log("键名" + key);
  console.log("键值" + obj[key]);
}
  • 它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。
  • 它不仅遍历对象自身的属性,还遍历继承的属性

因为 for in 会遍历继承而来的属性,多数不需要,所以最好结合 hasOwnProperty 判断过滤一下

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

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

# 三、with 语句

格式

with (对象) {
  语句;
}

作用:操作同一对象的多个属性时,书写方便。

var obj = {
   p1 : 1,
   p2 : 2
}

with(obj){     =>     obj.p1 = 3; obj.p2 =4
   p1 = 3,
   p2 = 4
}

如果对象中没有的值在 with 中修改会创建全局变量,这是with语句的一个很大的弊病,就是绑定对象不明确。 所以尽量不用,或用临时变量替代。


2-26


# 2、函数

# 一、概述

# 1.1 函数的声明

JavaScript 有三种声明函数的方法。

(1)function 命令声明函数

 ```js

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


​ (2)函数表达式, 匿名函数变量赋值的写法

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


如果带有函数名,只能在内部访问,外部访问不到

​```js
var f = function f() {};

这种写法目的:1、方便除错 2、方便函数内部调用自身

(3)Function 构造函数

var add = new Function(
 'x',
 'y',
 'return x+y'
)
=>
function add(x,y){
  return x+y
}

Function 构造函数除了最后一个是函数体,其他都是参数

var foo = new Function(
  'return "hello world";'
);
=>
var foo = Function(
  'return "hello world";'
);

这种方式非常不直观,几乎无人用

  • 函数如果重复声明,后一次的函数声明会覆盖前面一次的

# 1.2 圆括号运算符,return 语句和递归

  • 圆括号运算符中传入函数的参数

  • return 后面跟表达式,后面语句不会继续执行

  • 没有 return,不反回,或者说返回 undefined

  • 函数可以调用自身,也就是递归函数

funciton fib (num){
   if(num ===0)return 0;
   if(num ===1) return 1;
   return fib(num-2)+fib(num -1)
   fib(4) + fib(5)
   fib(2)+fib(3)+fib(3)+fib(4)
}
fib(6)  // 8

# 1.3 第一等公民

JavaScript 语言将函数看作一种值,与其他值地位相同,可以用值的地方就可以用函数,比如可以赋给变量,可以作为参数传入,可以作为对象的属性,可以作为函数结果返回。因为函数与其他数据类型地位平等,所以 JavaScript 语言又被称为函数的第一等公民。

function add(x, y) {
  return x + y;
}
// var operator = add;
// 将函数作为参数和返回值
function a(op) {
  return op;
}
a(add)(1, 1);

# 1.4 函数名的提升

JavaScript 引擎将函数名视为同变量名,所以 function 声明函数时,整个函数会像变量声明一样,被提升到代码头部

f(); // 不报错

function f() {}

如果是赋值语句

f()  //  TypeError: undefined is not a function
var f = function(){}
=>
var f;
f()
f = function(){}

如果同时采用 function 命令和赋值语句声明同一个函数,最后总是采用赋值语句的定义。

# 二、函数的属性和方法

# 2.1 name 属性

函数的name属性返回函数的名字

function f1(){}
f1.name // 'f1'

var f2 = function(){}
f2.name. // 'f2'

var f3 = function myName(){}
f3.name.   // myName

应用
var test = function myFun(){}
function g(f){f.name}
g(test)  // myFun

# 2.2 length 属性

function a(x,y){}
a.length. // 2

# 2.3 toString()

函数的 toString 方法返回一个字符串,内容就是函数的源码

如果是原生的函数,返回function (){[natice code]}

函数内部注释也可以返回,可以利用此返回多行文本

function f() {
  /*
   这是一个多行文本
  */
}
f.toString()
  .split("\n")
  .slice(2, -2)
  .join("\n");

# 三、 函数作用域

# 3.1 定义

作用域(scope)指变量的存在范围,ES5 中,JS 有两种作用域,一种是全局作用域,一种是函数作用域。ES6 又新增了块级作用域。

对于顶层函数来说,外部声明的变量是全局变量,内部的是局部变量,只能在内部访问。

var a = 1;
function test() {
  var a = 3;
  console.log(a); // a = 3  该作用域内局部变量会覆盖全局变量
}
console.log(a); // a=1
test();

由于 var 只有函数作用域,所以除了函数外,任何区块声明都是全局变量

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

# 3.2 函数内部的变量提升

与全局作用域一样,函数作用域内部也会产生“变量提升”现象。var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。

function foo(x){
  if(x>100){
     var tmp = x -100;
  }
}
=>
function foo(x){
   var tmp;
   console.log(tmp)   // 不报错 undefined
   if(x>100){
      tmp = x -100
   }
}

# 3.3 函数本身的作用域

函数本身也是一个值,也有自己的作用域,他的作用域之和声明时所在的作用域有关,与运行时所在的作用域无关。

var a = 1;
var x = function test() {
  console.log(a); // 1  // 全局没有定义a时,为undefined
};
function f(x) {
  var a = 2;
  x();
}
f();

函数体内部声明的函数,作用域绑定函数体内部,这是这种机制,构成了闭包

function a() {
  var foo = 1;
  return function b() {
    console.log(foo); // 1
  };
}

var foo = 2;
a()(); // 1

# 四、参数

# 4.1 概述

函数运行的时候,有时需要提供外部数据,不同的外部数据会得到不同的结果,这种外部数据就叫参数。

# 4.2 参数的省略

函数参数不是必需的,JavaScript 允许省略参数。

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

f(1, 2, 3); // 1
f(1); // 1
f(); // undefined  可以通过es6给参数设置默认值

f.length; // 2

# 4.3 传递方式

原始类型的值(布尔、数值、字符串),参数传递到函数,函数体内修改不会影响原始值,

复合类型的值(数值、对象、其他函数),传递进来的值修改后会影响原始值。如不想修改原始值,需

要克隆一份进行修改

# 4.4 同名参数

函数传入两个参数同名,取最后一个的值

function f(a, a) {
  // 默认是取第二个参数,但是第二个是undefined 所以打印undefined,但是如果想取第一个的参数可以用arguments取参
  console.log(arguments[0]);
}

f(1); // 1

# 4.5 arguments 对象

函数允许有不通数目的参数,需要一种机制读取所有的参数,这就是 arguments 对象。

这个对象包含了函数运行时所有的参数。

const f = functin(one){
   console.log(arguments[0])
   console.log(arguments[1])
   console.log(arguments[2])
}

正常模式下 arguments 对象可以在运行时修改值,但是严格模式下不可以

arguments.length 判断运行时,传入的参数个数

虽然 arguments 像数组但不是数组,slice 和 forEach 等都不可用

可以先转化成数组,转化成数组的方法是

var args = Array.prototyoe.slice.call(arguments);
// 或者
var args = [];
for (var i = 0; i < arguments.lenght; i++) {
  args.push(arguments[i]);
}

# 五、函数其他知识点

# 5.1 闭包

  1. 闭包(closure) 是JavaScript语言一个难点,也是特色,很多高级应用都要依靠闭包实现。

  2. Javascript 有两种作用域,全局作用域和函数作用域,函数内部可以读取全局变量,但是函数外部却无法读取函数内部声明的变量。

  3. 所以想要获取函数内部的变量,需要在函数内部再定义一个函数,读取到值后,在将这个函数和它读取的值一起返回。表现为函数嵌套函数的形式。

  4. JavaScript 语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。父对象的所有变量,对子对象都是可见的,反之则不成立。

  5. 用处:(1)读取函数内部变量 (2)让变量保持在内存中,不被销毁

  6. 缺点:易造成内存泄露,外层函数每次运行都护生成一个新的闭包,这个闭包又会保留外层函数的内部变量,所以内存消耗很大,因此不能滥用闭包,负责容易造成网页性能问题。

  7. 解决:将用完的函数或者变量置为 null。

function f1() {
  var a = 1;
  return function() {
    return a;
  };
}
f1()(); // 1

// 没执行一次加一
function createIncrementor(start) {
  return function() {
    return start++;
  };
}
var inc = createIncrementor(5);
inc(); // 5
inc(); // 6
inc(); // 7
  • 利用闭包实现点赞效果
<body>
   <button></button>
   <button></button>
   <button></button>
</body>
<script>
    const buttons = document.getElementsByTagName('button')
    const nums = [20,31,0]
    for(var i=0;i<buttons.length;i++){
        buttons[i].innerHTML = `点赞${nums[i]}`
        buttons[i].onclick=fi(nums[i])
        // 显示点击的按钮索引
        // buttons[i].onclick=(function(i){
        //    return function(){
        //        console.log(i,'iii')
        //    }
        // })(i)
    }
    function fi(count){
        return function(){
            this.innerHTML = `点赞${++count}`
        }
    }
</script>
  • 实现一个 getter、setter 方法
var getValue, setValue;
(function() {
  var secret = 1;
  getValue = function() {
    return secret;
  };
  setValue = function(value) {
    secret = value;
  };
})();
alert(getValue()); // 1
setValue(4);
alert(getValue()); // 4
  • 封装私有化的属性和方法
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

# 5.2 立即调用的函数表达式(IIFE)

在 JavaScript 中,圆括号是一种运算符,跟在函数后面,表示调用该函数,比如print()就表示调用 print 函数。

function这个关键字既可以作为语句,也可以作为表达式。

// 语句
function f() {}

// 表达式
var f = function f() {};

JavaScript 引擎规定,如果 function 关键字在行首,就认为是函数语句,只要不让 function 出现在行首,即认为是一个表达式

所以一般用立即调用的函数表达式,注意表达式后面加 ';'

好处:(1)不必为函数命名,避免污染全局变量 (2)IIFE 内部形成单独的作用域,可以封装外部无法读取的私有变量

(function() {
  var tmp = new Data();
  storeData(tmp);
})();

# 六、eval 命令

# 6.1 基本用法

eval命令接受一个字符串作为参数,并将这个字符串当作语句执行。

eval("var a = 1;");
a; // 1

因为 eval 命令没有自己的作用域,有可能改写外部变量,具有安全风险,另外不利于 js 引擎优化执行速度,所以一般不推荐使用,常见的场合是解析 JSON 数据的字符串,但是正确做法是使用原声的 JSON.parse 方法

# 6.2 eval 的别名调用

只要不是eval( )调用,全是别名调用,javascript 规定,别名调用,作用域为全局

var a = 1;

function f() {
  var a = 2;
  var e = eval;
  e("console.log(a)");
}

f(); // 1

# 3、数组

# 一、 定义

数组是一次序排列的一组值。每个值的位置都有编号(从 0 开始),整个数组用方括号表示。

var arr = ["a", "b", "c"];
arr[3] = "d";
arr; // ['a','b','c','d'];
任何类型的数据都可以放入数组;
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}

数组的元素也是元素,称为多维数组

var a = [
  [1, 2],
  [3, 4]
];
a[0][1]; // 2
a[1][0]; // 3

# 二、 数组本质

  • 本质上,数组属于特殊的对象,typeof 运算符会返回数组的类型是object

  • 它的键名是依次排列的整数,Object.keys(arr) 可以看出

  • 取值和赋值可以用方括号结构 arr['1'] 或 arr[1] 数值会转化成字符串键值

# 三、 length 属性

  • 数组的 length,返回数组的成员数量
[1,2,3].length. // 3
  • 数组的 length 始终比最大的键值大于 1
const arr = [1, 2];
arr[999] = 9;
arr; // (1000) [1, 2, empty × 997, 9]
arr.length; // 1000
  • 设置 length 长度,可以改变数组的长度
// length小于数组长度
const arr = [1,2,3,4]
arr.length = 1
arr // [1,2]
arr.length = 0
arr = []

// length大于数组长度
arr.length = 99
arr = [1,2]
arr. // (99) [1, 2, empty × 97]

// 当取值为空时会返回undefined
const arr = [1,2]
arr[2] = 3  // undefined
  • 由于数组本质上是一种对象,数组添加属性后,length 不变
var a = [];
a.lenght; // 0
a["p"] = "123";
a; // [p:"123"]
a.length; // 0

# 四、in 运算符

检查某个键名是否存在的运算符in,对象和数组都是用

var arr = ["a", "b", "c"];
1 in arr; // true
"2" in arr; // true
5 in arr; // false  空值也会返回false

# 五、 for...in 循环 和数组遍历

  • for...in既可以遍历对象也可以遍历数组,但是不推荐遍历数组
var a = [1, 2, 3];
for (var i in a) {
  console.log(a);
}
a["foo"] = true;
// 1
// 2
// 3
// foo

for...in在遍历数组的时候不仅遍历数字键,也会遍历非整数键 foo,所以不提交用for...in遍历数组

  • 数组的便利可以考虑使用 for 循环或 while 循环
var arr = [1, 2, 3, 4, 5, 6, 7];
// for 循环
for (var i = 0; i < arr.length; i++) {
  console.log(arr[i]);
}
// while循环
var i = 0;
while (i < arr.length) {
  console.log(arr[i]);
  i++;
}
// while逆向遍历
var l = arr.length;
while (l--) {
  console.log(arr[l]);
}
// forEach 方法遍历
var color = ["red", "green", "blue"];
color.forEach(function(color) {
  console.log(color);
});
// red
// green
// blue

# 六、数组的空位

数组的空位指的是两个逗号之间没有任何值,最后有逗号不会产生空位,空位也占位置,算到 length 中

空位读取是 undefined

delete 删除会形成空位,并且不会影响 length 长度

var arr1 = [,,2,]
arr1.length  // 3

var arr2 = [,,2,,]
arr2.length  // 4
arr2[1] // undefined

var arr3 = [1,2,3]
delete arr3[1]
arr3 // [1,,3]
arr3.length // 3
arr3[1]   // undefined

遍历的情况下,无论 for...in 、Object.keys 还是 forEach 遍历,空位都会被跳过

var a = [, , ,];

a.forEach(function(x, i) {
  console.log(i + ". " + x);
});
// 不产生任何输出

for (var i in a) {
  console.log(i);
}
// 不产生任何输出

Object.keys(a);
// []

但是 undefined 不会

var b = [undefined, undefined];
a.forEach(function(x, i) {
  console.log(i + ". " + x);
});
// 0. undefined
// 1. undefined
// 2. undefined
for (var i in a) {
  console.log(i);
}
// 0
// 1
// 2
Object.keys(a);
// ['0', '1', '2']

# 七、类似数组的对象

如果有一个对象所有的键名都为正整数或 0,并且有 length 属性,那么就把这个对象称为"类似数组的对象(array-like object)"

var obj = {
  0: "a",
  1: "b",
  2: "c",
  length: 3
};
obj[0]; // 'a'
obj[1]; // 'b'
obj[2]; // 'c'
length; // 3
obj.push("d"); //  TypeError:obj.push is not a function

以上,对象 obj 就是一个类似数组的对象,但是,类似数组的对象并不是数组,也不具有数组特有的方法,对象 obj 不具有数组的 push 方法。

‘类似数组的对象‘的根本特征是,具有 length 属性,但是这个 length 不是动态变化的,不会随成员的变化而变化。说明不是真正的数组

var obj = {
  length: 0
};

obj[3] = "d";
obj.lenght; // 0

典型的“类似数组的对象”是函数的arguments对象,大多数 DOM 元素集,还有字符串。

// arguments对象
function args(){
    return arguments
}
const likeArr = args(1,2,3)
likeArr.length  // 3
likeArr[0]      //1
likeArr instanceof Array   // false

//DOM元素集
var elts = document.getElementsByTagName('h3');
elts.lenth  // 3
elts instanceof elts // false

// 字符串string
'abc'[1]      // 'b'
'abc'.length  // 3
'abc' instanceof Array. // false

以上三个例子很像数组,但是不是真正的数组,但是slice可以将他们变成真正的数组。

var likeArr = "weopp";
var arr = Array.prototype.slice.call(likeArr);
// 等价于 Array.from(likeArr)
arr; // ['w', 'e', 'p' , 'p']

arrayLike代表一个类似数组的对象,本来是不可以使用数组的forEach()方法的,但是通过call(),可以把forEach()嫁接到arrayLike上面调用

// 1、把数组的forEach的方法嫁接到类数组对象上去使用,通过call 执行方法
var arrayLike = "sa"
Array.prototype.forEach.call(arrayLike, (value, index)=>{
    console.log(index + ' : ' + value);
});
// 0 : s
// 1 : a

// 2、等同于 for 循环
function logArgs() {
  for (var i = 0; i < arguments.length; i++) {
    console.log(i + '. ' + arguments[i]);
  }
}

//3、通过slice先转成数组,在遍历 等同于 Array.from()
Array.prototype.slice.call('123').forEach(...)
Array.from('123').forEach(...)

// 4、es6中,解构[...s]可以转化为数组


2020-02-28