js数据类型
基本数据类型(七种)
`boolean`
`null`
`undefined`
`number`
`string`
`symbol`: 独一无二
let s1 = Symbol('another symbol')
let s2 = Symbol('another symbol')
s1 === s2 // false
`BigInt`: JS 第二个数字数据类型
引用类型
Array、Function、Object、RegExp、Date
基本数据类型和引用数据类型区别
一、声明变量时内存分配不同
原始类型:占据空间是固定,存在较小的内存栈中,这样便于迅速查询变量的值
引用类型:存在堆中,栈中存储的变量,只是用来查找堆中的引用地址。
二、不同的内存分配带来不同的访问机制
在javascript中是不允许直接访问保存在堆内存中的对象的,所以在访问一个对象时,首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值,这就是传说中的按引用访问。而原始类型的值则是可以直接访问到的。
三、复制变量时的不同
1. 原始值:在将一个保存着原始值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量是完全独立的,他们只是拥有相同的value而已。
2. 引用值:在将一个保存着对象内存地址的变量复制给另一个变量时,会把这个内存地址赋值给新变量,也就是说这两个变量都指向了堆内存中的同一个对象,他们中任何一个作出的改变都会反映在另一个身上。(这里要理解的一点就是,复制对象时并不会在堆内存中新生成一个一模一样的对象,只是多了一个保存指向这个对象指针的变量罢了)。多了一个指针
四、参数传递的不同(把实参复制给形参的过程)
首先我们应该明确一点:ECMAScript中所有函数的参数都是按值来传递的。
但是为什么涉及到原始类型与引用类型的值时仍然有区别呢?还不就是因为内存分配时的差别。
1. 原始值:只是把变量里的值传递给参数,之后参数和这个变量互不影响。
2. 引用值:对象变量它里面的值是这个对象在堆内存中的内存地址,这一点你要时刻铭记在心!
因此它传递的值也就是这个内存地址,这也就是为什么函数内部对这个参数的修改会体现在外部的原因了,因为它们都指向同一个对象。
原型
new
的实现原理:
- 创建一个空对象,构造函数中的this指向这个空对象
- 这个新对象被执行 [[原型]] 连接
- 执行构造函数方法,属性和方法被添加到this引用的对象中
- 如果构造函数中没有返回其它对象,那么返回this,即创建的这个的新对象,否则,返回构造函数中返回的对象。
创建对象的几种方法
1. 字面两创建
var o1 = {name: 'o1'}
2. new 操作符 + Object 创建对象
var o2 = new Object({name:'o11'})
3. 显式的构造函数
var M = function(){this.name = 'o2'}
var o2 = new M();
4. Object.create 方法
var P = {name: 'o3'}
var P2 = Object.create(P)
原型链
所有的JS对象都有一个prototype属性,指向它的原型对象。当试图访问一个对象的属性时,如果没有在该对象上找到,它还会顺着proto
(隐式原型)obj.__proto__.__proto__
该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾(null).
- 原型对象也是普通的对象,是对象一个自带隐式的 proto 属性,原型也有可能有自己的原型,如果一个原型对象的原型不为 null 的话,我们就称之为原型链。
- 原型链是由一些用来继承和共享属性的对象组成的(有限的)对象链
prototype 和 __proto__
构造函数拥有 prototype,对象没有。
只有实例对象拥有 __proto__
(函数也是一个对象)
var A = function(name) { this.name = name }
A.__proto__ === Function.prototype
A 是 Function 的实例,所以函数也拥有__proto__
__proto__
访问器我们可以访问对象的[[Prototype]
instanceof 和 typeof
typeof null // "object" `null`:所有机器码均为0, 000:对象
null instanceof null // TypeError: Right-hand side of 'instanceof' is not an object
Object instanceof Object // true
Function instanceof Function // true
Function instanceof Object // true
function Foo() { }
Foo instanceof Foo // false
Foo instanceof Object // true
Foo instanceof Function // true
Object.prototype.toString.call() // 完美解决方案
使用 typeof
来判断基本数据类型是 ok 的,不过需要注意当用 typeof
来判断 null
类型时的问题,如果想要判断一个对象的具体类型可以考虑用 instanceof
,但是 instanceof
也可能判断不准确,比如一个数组,他可以被 instanceof
判断为 Object。所以我们要想比较准确的判断对象实例的类型时,可以采取 Object.prototype.toString.call
方法。
实现一个instanceof方法:
function new_instance_of(leftVaule, rightVaule) {
let rightProto = rightVaule.prototype; // 取右表达式的 prototype 值
leftVaule = leftVaule.__proto__; // 取左表达式的__proto__值
while (true) {
if (leftVaule === null) {
return false;
}
if (leftVaule === rightProto) {
return true;
}
leftVaule = leftVaule.__proto__
}
}
new 的过程
1. 新生成了一个对象
2. 新对象隐式原型链接到函数原型
3. 调用函数绑定this
4. 返回新对象
手动实现一个new
function _new(fun) {
return function() {
let obj = {
__proto__: fun.prototype
}
fun.apply(obj, arguments)
return obj
}
}
function person(name, age) {
this.name = name
this.age = age
}
let obj = _new(person)('LL', 100)
let obj2 = new person('LL', 100)
console.log(obj) //{name: 'LL', age: 100}
console.log(obj2) //{name: 'LL', age: 100}
ES5 官方文档在 函数定义:
调用函数person,将其返回值赋给 result;
其中,person 执行时的实参为传递给 \[\[Construct\]\](即person本身) 的参数,person 内部 this 指向 obj;
如果 result 是 Object 类型(基本数据类型除外),返回 result;
这也就解释了如果构造函数显式返回对象类型,则直接返回这个对象,而不是返回最开始创建的对象。
JavaScript中判断函数是new
方法一: instanceof
function Person(n,a){
this.name = n;
this.age = a;
if(this instanceof Person){
alert('new调用');
}else{
alert('函数调用');
}
}
var p = new Person('jack',30); // --> new调用
Person(); // --> 函数调用
方法二: instanceof
function Person(n,a){
this.name = n;
this.age = a;
// arguments.callee 可代替换 Person
// if(this.constructor === arguments.callee){
if(this instanceof arguments.callee){
alert('new调用');
}else{
alert('函数调用');
}
}
var p = new Person('jack',30); // --> new调用
Person(); // --> 函数调用
arguments.callee它可以用于引用该函数的函数体内当前正在执行的函数。这在函数的名称是未知时很有用,例如在没有名称的函数表达式 (也称为“匿名函数”)内。【严格模式下禁用】
ES5 ES6继承
组合继承
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() { console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
Child.prototype = new Parent()
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
以上继承的方式核心是在子类的构造函数中通过Parent.call(this)
继承父类的属性,然后改变子类的原型为new Parent()
来继承父类的函数。
这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
寄生组合继承
这种继承方式对组合继承进行了优化,组合继承缺点在于继承父类函数时调用了构造函数,我们只需要优化掉这点就行了。
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype)
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。
以上两种继承方式都是通过原型去解决的,在 ES6 中,我们可以使用class
去实现继承,并且实现起来很简单
Class 继承
以上两种继承方式都是通过原型去解决的,在 ES6 中,我们可以使用class
去实现继承,并且实现起来很简单
class Parent {
constructor(value) {
this.val = value
}
getValue() {
console.log(this.val)
}
}
class Child extends Parent {
constructor(value) {
super(value)
}
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
class
实现继承的核心在于使用extends
表明继承自哪个父类,并且在子类构造函数中必须调用super
,因为这段代码可以看成Parent.call(this, value)
。
当然了,之前也说了在 JS 中并不存在类,class
的本质就是函数。
函数防抖和函数节流
1. 函数防抖和函数节流都是防止某一时间频繁触发,但是这两兄弟之间的原理却不一样。
2. 函数防抖是某一段时间内只执行一次,而函数节流是间隔时间执行。
区别: 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。
结合应用场景
-
debounce
- search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
- window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
function debounce(fn) {
// 4、创建一个标记用来存放定时器的返回值
let timeout = null;
return function() {除
clearTimeout(timeout);
// var args = Array.prototype.slice.call(arguments)
var args = [...arguments];
timeout = setTimeout(() => {
fn.apply(this, args);
}, 1000);
};
}
function sayDebounce(){
console.log("防抖成功!");
}
btn.addEventListener("click", debounce(sayDebounce));
function debounce(func,wait,immediate) {
let timer;
return function () {
let context = this;
let args = arguments;
if (timer) clearTimeout(timer);
if (immediate) {
var callNow = !timer;
timer = setTimeout(() => {
timer = null;
}, wait)
if (callNow) func.apply(context, args)
} else {
timer = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
}
-
throttle
- 鼠标不断点击触发,mousedown(单位时间内只触发一次)
- 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断
var throttle = function(func, delay) {
var prev = Date.now();
return function() {
var context = this;
var args = arguments;
var now = Date.now();
if (now - prev >= delay) {
func.apply(context, args);
prev = Date.now();
}
}
}
function handle() {
console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));
// 处理函数
function handle() {
console.log(Math.random());
}
// 滚动事件
window.addEventListener('scroll', debounce(handle, 1000));
JS 事件循环
所以 Event Loop 执行顺序如下所示:
* 首先执行同步代码,这属于宏任务
* 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
* 执行所有微任务
* 当执行完所有微任务后,如有必要会渲染页面
* 然后开始下一轮 Event Loop,执行宏任务中的异步代码,也就是`setTimeout`中的回调函数
微任务包括: process.nextTick
,promise
,MutationObserver
,其中process.nextTick
为 Node 独有。
宏任务包括:script
,setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
,MessageChannel
。
这里很多人会有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了script
,浏览器会先执行一个宏任务,接下来有异步代码的话才会先执行微任务。
setImmediate->MessageChannel->setTimeout
,所以Vue(2.5+)
内部的nextTick
与2.4
及之前的实现是不一样的,需要注意下。
setTimeout 和 setInterval
用setTimeout
模拟定期计时和直接用setInterval
是有区别的:
1、 每次setTimeout
计时到后就会去执行,然后执行一段时间后才会继续setTimeout
,中间就多了误差
2、 而setInterval
则是每次都精确的隔一段时间推入一个事件(但是,事件的实际执行时间不一定就准确,还有可能是这个事件还没执行完毕,下一个事件就来了)
而且setInterval
有一些比较致命的问题:
1. 累积效应,如果`setInterval`代码在`setInterval`再次添加到队列之前还没有完成执行,就会导致定时器代码连续运行好几次,而之间没有间隔,就算正常间隔执行,多个`setInterval`的代码执行时间可能会比预期小(因为代码执行需要一定时间)
2. 比如你`ios`的`webview`,或者`safari`等浏览器中都有一人特点,在滚动的时候是不执行`JS`的,如果使用了`setInterval`,会发现在滚动结束后会执行多次由于滚动不执行`JS`积攒回调,如果回调执行时间过长,就会非常容易造成卡顿问题和一些不可知的错误(`setInterval`自带的优化,如果当前事件队列中有`setInterval`的回调,不会重复添加回调)
3. 而且把浏览器最小化显示等操作时,`setInterval`并不是不执行程序,它会把`setInterval`的回调函数放在队列中,等浏览器窗口再次打开时,一瞬间全部执行
所以,至于这么问题,一般认为的最佳方案是:用setTimeout
模拟setInterval
或者特殊场合直接用requestAnimationFrame
// setTimeout 实现 setInterval
function mySetInterval(fn, millisec){
function interval(){
setTimeout(interval, millisec);
fn();
}
setTimeout(interval, millisec)
}
// Uncaught RangeError: Maximum call stack size exceeded
function bar() {
bar()
}
bar()
当我们使用递归的时候,因为栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话,就会出现爆栈的问题
作用域
每一个变量、函数都有其作用的范围,超出作用不得使用,这个叫做作用域。
全局变量
- 在全局范围内声明的变量,如
var a=1;
- 只有赋值没有声明的值,如
a=2;
(注:如果a=2在函数环境中,也是全局变量
局部变量
写入函数中的变量,叫做局部变量。
作用
1. 程序的安全。
2. 内存的释放。
作用域链:
查找量的过程。先找自己局部环境有没有声明或者是函数,如果有,则查看声明有无赋值或者是函数的内容,如果没有,则向上一级查找。
预解析顺序:
每个程序都要做的工作,程序开始先预解析语法,标点符号是否有误,解析内存是否可容纳,解析变量……直到解析无误了,才开始按正常的流程顺序走。试想一下,如果没有预解析顺序,直接按流程顺序走,可能程序执行到最后一个函数,发现了语法错误,才开始报错,那性能要有多差啊!
顺序内容:
1.文件内引用的<script>块依次解析,从上到下连成一片。
2.每个script块内部的var(注意:只解析变量名,不解析值,如var a=2;将var a解析在环境的开头,并不解析后面的值,只有当程序执行到var a=2这行时,才会给变量赋值),function解析到本块的开头。
3.依次解析每个环境,将var,function解析到环境的开头。
闭包
闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包最常用的方式就是在一个函数内部创建另一个函数。
闭包的作用有:
1. 参数和变量不会被垃圾回收机制所收回
1. 封装私有变量,模仿块级作用域(ES5中没有块级作用域),实现JS的模块
自由变量的查找是在函数定义的地方,向上级查找,而不是函数执行的地方。
作用域应用的特殊情况,有两种表现形式
1. 函数作为返回值被返回
2. 函数作为参数被传递
// 函数作为返回值被返回
function create() {
let a = 100
return function () {
console.log(a)
}
}
let fn = create()
let a = 200
fn() // 100
// 函数作为参数被传递
function print(fn) {
let a = 200
fn()
}
let a = 100
function fn() {
console.log(a)
}
print(fn) // 100
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。