常问基础js
1.1 基本的数据类型介绍,及值类型和引用类型的理解
值类型(基本类型):
字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)、Symbol。
引用数据类型:对象(Object)、数组(Array)、函数(Function)。
注:Symbol 是 ES6 引入了一种新的原始数据类型,表示独一无二的值。
值类型
let a = 100;
let b = a;
a = 200;
console.log(b); // 100
值类型是直接存储在**栈(stack)**中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
引用类型
let a = { age: 20 };
let b = a;
b.age = 30;
console.log(a.age); // 30
引用类型存储在**堆(heap)**中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;
1.2 数据类型的判断
typeof 所有值类型判断 但是 null、对象、数组无法判断
instanceof 原型链逐级属性判断 区分自定义对象类型
object.prototype.toString.call() 原始数据类型判断 弥补typeof无法判断的内容 比较全
- typeof:能判断所有值类型,函数。不可对 null、对象、数组进行精确判断,因为都返回 object
console.log(typeof undefined); // undefined
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof "str"); // string
console.log(typeof Symbol("foo")); // symbol
console.log(typeof 2172141653n); // bigint
console.log(typeof function () {}); // function
// 不能判别
console.log(typeof []); // object
console.log(typeof {}); // object
console.log(typeof null); // object
- instanceof:能判断对象类型,不能判断基本数据类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。比如考虑以下代码:
class People {}
class Student extends People {}
const vortesnail = new Student();
console.log(vortesnail instanceof People); // true
console.log(vortesnail instanceof Student); // true
- instanceof 其实现就是顺着原型链去找,如果能找到对应的 Xxxxx.prototype 即为 true 。比如这里的vortesnail 作为实例,顺着原型链能找到 Student.prototype 及 People.prototype ,所以都为 true 。
- hasOwnProperty 查找属性 检查的就是 object 自身的对象,并不会检查原型链
- Object.prototype.toString.call():所有原始数据类型都是能判断的,还有 Error 对象,Date 对象等。
Object.prototype.toString.call(2); // "[object Number]"
Object.prototype.toString.call(""); // "[object String]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(Math); // "[object Math]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(function () {}); // "[object Function]"
那为什么不直接用obj.toString()呢?
需要调用的toString是Object的原型方法,
而Array 、Function等类型作为Object的实例,都重写了toString方法。
根据原型链的知识,优先调用的是重写之后的toString方法
所以采用obj.toString()不能得到其对象类型,应该调用Object上原型toString方法。
为什么要加prototype呢
Object对象和它的原型链上各自有一个toString()方法,第一个返回的是一个函数,第二个返回的是值类型。
// Array String ... 都是构造函数
Object.toString.call(Array) //'function Array() { [native code] }'
Object.prototype.toString.call(Array)//'[object Function]'
Object.prototype.toString.call([1,2,3,4])// '[object Array]'
Object.toString.call([1,2,3,4])// error VM1284:1 Uncaught TypeError: Function.prototype.toString requires that 'this' be a Function at Array.toString(<anonymous>) at <anonymous>:1:18
判断是否为数组的几种方法
Array.isArray(arr); // true
arr.__proto__ === Array.prototype; // true
arr instanceof Array; // true
Object.prototype.toString.call(arr); // "[object Array]"
ES6 新属性
Set: ES6 提供了新的数据结构。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Map: ES6 提供了新的数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键
0.1+0.2 !== 0.3
进制转换 :js 在做数字计算的时候,0.1 和 0.2 都会被转成二进制后无限循环 ,但是 js 采用的 IEEE 754 二进制浮点运算,最大可以存储 53 位有效数字,于是大于 53 位后面的会全部截掉,将导致精度丢失。。
- Number.EPSILON实际上是ES6 新增的 能够表示JavaScript 的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。一般来说为 Math.pow(2, -52) ,
function isEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
console.log(isEqual(0.1 + 0.2, 0.3)); // true
原型链
函数this指向
由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。
https://www.ruanyifeng.com/blog/2018/06/javascript-this.html
作用域
https://juejin.cn/post/6844903797135769614
for (let i = 0; i < 10; i++) {
// ...
}
console.log(i);
// ReferenceError: i is not defined
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
变量i是var命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,
// let
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
//闭包
var a = [];
for (var i = 0; i < 10; i++) {
(function(i){a[i] = function () {
console.log(i);
};})(i)
}
a[6](); // 6
如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6。
变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,
另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域
如果没有重新定义j 则循环体内部可以得到父级的继承值 j
如果重新定义 j 则循环体内部报错 在初始化 j 之前不能使用 j
直接改写 j 打印出 j 重新定义的内容
执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变。
手动实现call
call: 手动给 obj 增加了一个 fn 属性,我们执行完再使用对象属性的删除方法(delete)删除fn
Function.prototype.myCall = function (context) {
// 判断调用对象 this目前就是fn
if (typeof this !== "function") {
throw new Error("Type error");
}
// 获取非context的其他参数 移植到新函数中
let args = [...arguments].slice(1);
let result = null;
// 判断 context 是否传入,如果没有传就设置为 window
context = context || window;
// 在context上添加函数的this
// this 即为我们要调用的方法
context.fn = this;
// 执行要被调用的方法
result = context.fn(...args);
// 删除手动增加的属性方法
delete context.fn;
// 将执行结果返回
return result;
};
var obj = {
value: "test",
name:'cld'
};
function fn() {
console.log(this.value);
}
fn.myCall(obj)
打印 this与context
call,apply,bind的基本介绍
改变函数执行时的this指向,目前所有关于它们的运用,都是基于这一点来进行的
语法
- fun.call(thisArg, param1, param2, …)
- fun.apply(thisArg, [param1,param2,…])
- fun.bind(thisArg, param1, param2, …)
调用call/apply/bind的必须是个函数
call、apply和bind是挂在Function对象上的三个方法,只有函数才有这些方法。
只要是函数就可以,比如:
Object.prototype.toString就是个判断类型函数 Object.prototype.toString.call(data)
- apply是以a开头,它传给fun的参数是Array,也是以a开头的。
- call/apply改变了函数的this上下文后马上执行该函数
- bind则是返回改变了上下文后的函数,不执行该函数
防抖节流
防抖
将多次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
缺点:如果事件在规定的时间间隔内被不断的触发,则调用方法会被不断的延迟
//防抖debounce代码:
function debounce(fn,delay) {
var timeout = null; // 创建一个标记用来存放定时器的返回值
return function (e) {
// 每当用户输入的时候把前一个 setTimeout clear 掉
clearTimeout(timeout);
// 然后又创建一个新的 setTimeout, 这样就能保证interval 间隔内如果时间持续触发,就不会执行 fn 函数
timeout = setTimeout(() => {
fn.apply(this, arguments);
}, delay);
};
}
// 处理函数
function handle() {
console.log('防抖:', Math.random());
}
window.addEventListener('scroll', debounce(handle,500));
函数节流(throttle)
函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,
实现方式:每次触发事件时,如果当前有等待执行的延时函数,则直接return不执行
//节流throttle代码:
function throttle(fn,delay) {
let canRun = true; // 通过闭包保存一个标记
return function () {
// 在函数开头判断标记是否为true,不为true则return
if (!canRun) return;
// 立即设置为false
canRun = false;
// 将外部传入的函数的执行放在setTimeout中
setTimeout(() => {
// 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。
// 当定时器没有执行的时候标记永远是false,在开头被return掉
fn.apply(this, arguments);
canRun = true;
}, delay);
};
}
function sayHi(e) {
console.log('节流:', e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi,500));