1.数据类型
JavaScript一共有8种数据类型
其中有7种基本数据类型:
- Undefined (表示定义,但未赋值)
- Null (定义且赋值为null)
- Boolean
- Number
- String
- Symbol(es6新增,表示独一无二的值)
- BigInt(es10新增)
一种引用数据类型:
- Object (Object本质上是由一组无序的键值对组成的)
原始数据类型:
- 直接存储在栈(stack)中,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储。
引用数据类型:
- 同时存储在栈(stack)和堆(heap)中,占据空间大、大小不固定。
- 引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。
- 当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
2.类型转换
在 JS 中类型转换只有三种情况,分别是:
- 转换为布尔值(调用Boolean()方法) ```javascript // Number => Boolean // 除了 0 和 NaN 为 false,其他情况为true
// String => Boolean // 空字符串会转换为false,其他情况为true
// undefined, null 都会转换为 false
// 引用数据类型会转换为 true
-转换为数字(调用Number()、parseInt()和parseFloat()方法)```javascript// 纯数字字符串 转换为 数字// 非纯数字字符串 装换为 NaN// 空数组 转换为 数字 0// [1] 或 ['1']这种只有一个元素且这个元素为数字或数字字符串时 装换为 这个数字, 这里转换为 1// 非数组的引用数据类型 转换为 NaN
- 转换为字符串(调用.toString()或者String()方法) ```javascript // 需要注意 // null 和 undefined 没有 toString 方法, 可以使用 String 方法
// Number、Function、Boolean、Symbol、null、undefined 会得到对应的字符串
// [1, 2] => “1, 2”
// Object => “[object Object]”
<a name="cedd72f5"></a>## 3.数据类型的判断<a name="4678cdc6"></a>##### (1) typeof```javascript// 可以用来判断 非null 的基本数据类型typeof 1 // "number"typeof true // "boolean"typeof 'str' // "string"typeof undefined // "undefined"typeof null // "object"typeof [] // "object"typeof Function // "function"typeof {} // "object"
(2) instanceof
// instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。2 instanceof Number // falsetrue instanceof Boolean // false'str' instanceof String // false[] instanceof Array // truefunction(){} instanceof Function // true{} instanceof Object // true/*instanceof 在MDN中的解释:instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。其意思就是判断对象是否是某一数据类型(如Array)的实例。在这里字面量值,2, true ,'str'不是实例,所以判断值为false。*/
(3) constructor
// constructor 通过构造函数来判断类型,记得要加括号(2).constructor === Number // true(true).constructor === Boolean // true('str').constructor === String // true([]).constructor === Array // true(function(){}).constructor === Function // true({}).constructor === Object // true// 使用这个需要注意一个问题:如果被修改过原型,则判断不再准确。
(4) Object.prototype.toString.call()
Object.prototype.toString.call(2) // '[object Number]'Object.prototype.toString.call(true) // '[object Boolean]'Object.prototype.toString.call('str') // '[object String]'Object.prototype.toString.call([]) // '[object Array]'Object.prototype.toString.call(function(){}) // '[object Function]'Object.prototype.toString.call({}) // '[object Object]'
4.null 和 undefined 的区别
- 首先 null 和 undefined 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 null 和 undefined。
- null 代表的含义是空对象(其实不是真的对象),主要用于赋值给一些可能会返回对象的变量,作为初始化
- undefined 代表的含义是未定义
其实 null 不是对象,虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
5.作用域和作用域链
作用域: 作用域是定义变量的区域,它有一套访问变量的规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量(标识符)进行变量查找。
作用域链: 作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和 函数。
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前 端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。
当我们查找一个变量时,如果当前执行环境中没有找到,我们可以沿着作用域链向后查找。
6.break和continue
- break 和 continue 语句用于在循环中精确地控制代码的执行
- 使用break语句会使程序立刻退出最近的循环,强制执行循环 后边的语句
- 使用continue语句会使程序跳过当次循环,继续执行下一次循 环,并不会结束整个循环
7. 创建对象的方式
在 JavaScript 中虽然 Object 构造函数或对象字面量都可以用来创建单个对象,但是这些方法都有一个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。
(1) 工厂函数创建对象
function createPerson(name, age, job){let person = new Object();person.name = name;person.age = age;person.job = job;person.sayName = function(){alert(this.name);};return person;}let person1 = createPerson("james",9,"student");let person2 = createPerson("kobe",9,"student");
- 优点:解决了创建多个相似对象时,代码的复用问题
- 缺点:使用工厂模式创建的对象,没有解决对象识别的问题(就是怎样知道一个对象的类型是什么)
(2)构造函数模式
function Person(name, age, job){this.name = name;this.age = age;this.job = job;this.sayName = function(){alert(this.name);};}let person1 = new Person("james",9,"student");let person2 = new Person("kobe",9,"student");
- 当我们使用构造函数实例化一个对象的时候,对象中会包含一个
__proto__属性指向构造函数原型对象,而原型对象中则包含一个 constructor 属性指向构造函数。因此在实例对象中我们可以通过原型链来访问到 constructor 属性,从而判断对象的类型。 - 优点:解决了工厂模式中对象类型无法识别的问题,并且创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型。
- 缺点:我们知道 ECMAScript 中的函数是对象,在使用构造函数创建对象时,每个方法都会在实例对象中重新创建一遍。拿上面的例子举例,这意味着每创建一个对象,我们就会创建一个 sayName 函数的实例,但它们其实做的都是同样的工作,因此这样便会造成内存的浪费。
(3) 原型模式(优化构造函数的写法)
我们知道,我们创建的每一个函数都有一个 prototype 属性,这个属性指向函数的原型对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。我们通过使用原型对象可以让所有的对象实例共享它所包含的属性和方法,因此这样也解决了代码的复用问题。如下面所示:
function Person(name, age){this.name = name;this.age = age;}// 避免了每一次创建对象都要新建一个系统的sayName函数,节省了内存空间Person.prototype.sayName = function(){alert(this.name);}Person.prototype.job = 'student';var person1 = new Person('james', 9);person1.sayName(); // "james"var person2 = new Person('kobe', 9);person2.sayName(); // "kobe"console.log(person1.sayName === person2.sayName) // true
8. call、apply和bind
call、apply 和 bind 都可以改变函数内部 this 的指向
call、apply 是立即调用,而 bind 是返回对应函数
三者在传递参数上也存在区别 ```javascript let obj = { name: ‘obj’, logName(str){
console.log(this.name, str)
} }
let obj1 = {name: ‘java’}; let obj2 = {name: ‘javaScript’}
// call 和 apply的区别在传参上 obj.logName(‘obj’); // obj obj obj.logName.call(obj1, ‘call’); // java call obj.logName.apply(obj2, [‘apply’]); // javaScript apply obj.logName.bind(obj1)(‘bind’) // java bind
<a name="028fb9b2"></a>## 9.闭包<a name="8750a9c9"></a>### 什么是闭包闭包是指有权访问另外一个函数作用域中的变量的函数。 可以理解为 闭包就是能够读取其他函数内部变量的函数 。闭包主要有两种表现:函数作为参数被传递、函数作为返回值被返回```javascriptfunction outer(){let name = '张三';let age = 18;let sayHello = function () {console.log(name, age);}// sayHello 就是一个闭包,因为它可以访问outer函数的作用域return sayHello;}
闭包的作用
闭包的最大用处有两个:一个是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
let add;function f1(){let num = 1;// add是个全局变量,被赋值了一个匿名函数。注意:这个匿名函数也是一个闭包add = ()=>{num += 1;}function f2(){console.log(num)}return f2}let result = f1();result(); // 1add();result(); // 2
在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是1,第二次的值是2。这证明了,函数f1中的局部变量num一直保存在内存中,并没有在f1调用后被自动清除。
为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
闭包的注意点
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
10.防抖与节流
函数防抖(debounce)
函数防抖:一个需要频繁触发的函数,在规定时间内,只让最后一次生效,前面的不生效。
// 实现原理:// 第一次执行函数时,创建一个定时器,在指定的时间间隔后执行对应的逻辑// 下一次执行函数时,如果存在定时器,清除上一次的定时器,再创建一个新的定时器function debounce(fn, delay) {let timer = null;// 用到了闭包的特性,可以使变量timer的值长期保存在内存中。return ()=>{timer && clearTimeout(timer);timer = setTimeout(()=>{fn();}, delay)}}
函数节流(throttle)
函数节流:函数在执行一次后,只有在大于设定的执行周期之后才会执行第二次
// 实现原理:/*用 时间戳 来判断是否已到回调该执行时间,记录上次执行的时间戳,然后每次触发 scroll 事件执行回调,回调中判断当前时间戳距离上次执行时间戳的间隔是否已经到达 规定时间段,如果是,则执行,并更新上次执行的时间戳,如此循环;*/function throttle(fn, delay) {// 记录上一次函数触发的时间let lastTime = 0;return ()=>{let nowTime = Date.now();if(nowTime - lastTime > delay){fn.call(this);lastTime = nowTime;}}}
11.原型与原型链
原型(prototype)
- 每个函数都有一个
prototype属性,它默认指向一个Object空对象(即称为:原型对象) - 原型对象中有一个属性
constructor,它指向函数对象
function Person(name, age) {this.name = name;this.age = age;}// Person.prototype.constructor 指向Person函数// 原型对象有什么用?// 可以通过原型对象给实例对象添加共享的方法和属性// 我想Person的实例对象都拥有一个sayHello方法,我就可以这样做:Person.prototype.sayHello = ()=>{console.log('hello world')}
显式原型与隐式原型
- 每个函数
function都有一个prototype属性,即 显式原型 - 每个实例对象都有一个
__proto__,可称为隐式原型,创建对象时自动添加的,默认值为构造函数的prototype属性值 - 实例对象的隐式原型的值
===其对应构造函数的显式原型的值 (prototype === __proto__) - 一般只操作显式原型,而不直接操作隐式原型
原型链
访问一个对象的属性时, 先在自身属性中查找,找到则返回。如果自身属性中没有,再沿着__proto__这条链向上查找,找到则返回。如果最终没有找到,返回undefined
