赞
踩
JavaScript一共有八种数据类型,其中分为基本数据类型和引用数据类型。
! ! 运算符可以将右侧的值强制转换成布尔值,同时这也是将值转换成布尔值的一种简单方法
注意:单个 ! 只是将右侧的值取反,例:
在JS中的数据类型转换有三种情况,分别是:
转换成布尔值(调用Boolean( ) 方法)
转换成数字(调用Number( ) 、parseInt( )、parseFloat( ) )
转换成字符串(调用toString( )、String( )方法)
对于基本类型数据来说,除了null其他的都可以显示正确的类型
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // string
console.log(typeof undefined); // undefined
console.log(typeof null); // object null 的数据类型被 typeof 解释为 object
而对于引用数据类型来说,除了函数其他的都会显示Object,所以当需要判断一个对象的正确数据类型时,可以考虑使用instanceof
console.log(typeof []); // object []数组的数据类型在 typeof 中被解释为 object
console.log(typeof function(){}); // function
console.log(typeof {}); // object
instanceof 可以正确的判断对象的数据类型,其原理是通过判断对象中的原型链能不能找到类型的prototype。即A instanceof B,如果A是B的实例,则返回true,否则返回false
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
可以看出直接通过字面量来 判断数据类型,instanceof可以很精确的判断出引用数据类型,而对于基本数据类型却不能精准的判断出来。
当一个函数被定义时,JS会给其添加prototype原型,然后在prototype上在添加一个constructor属性,并将其指向该函数的引用。因此可以通过这样的方式来判断数据类型。
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
注意:
function Fn(){};
Fn.prototype=new Array();
var f=new Fn();
console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true
使用Object对象的原型方法toString(),在通过call()来借用Object的toString()方法。
var a = Object.prototype.toString;
console.log(a.call(2)); //[object Number]
console.log(a.call(true)); //[object Boolean]
console.log(a.call('str')); //[object String]
console.log(a.call([])); //[object Array]
console.log(a.call(function(){})); //[object Function]
console.log(a.call({})); //[object Object]
console.log(a.call(undefined)); //[object Undefined]
console.log(a.call(null)); //[object Null]
var arr = []
Object.prototype.toString.call(arr) //"[object Array]"
arr.__proto__===Array.prototype //true
Array.isArray(arr) //true
arr instanceof Array //true
Array.prototype.isPrototypeOf(arr) //true
JS中的内置对象主要指的是在程序执行之前已经存在于全局作用域里的由JS定义的一些全局的值属性、函数属性和用来实例化其他对象的构造函数对象等。
标准内置对象的分类 (1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。 例如 Infinity、NaN、undefined、null 字面量 (2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。 例如 eval()、parseFloat()、parseInt() 等 (3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。 例如 Object、Function、Boolean、Symbol、Error 等 (4)错误对象,错误对象是一种特殊的基本对象。它们拥有基本的Error类型,同时也有多种具体的错误类型。 例如 Error、AggregateError、EvalError等 (5)数字和日期对象,用来表示数字、日期和执行数学计算的对象。 例如 Number、Math、Date (6)字符串,用来表示和操作字符串的对象。 例如 String、RegExp (6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。 例如 Array (7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。 例如 Map、Set、WeakMap、WeakSet (8)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。 例如 JSON 等 (10)控制抽象对象,控件抽象可以帮助构造代码,尤其是异步代码(例如,不使用深度嵌套的回调 ) 例如 Promise、Generator 等 (11)反射 例如 Reflect、Proxy (12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。 例如 Intl、Intl.Collator 等 (13)WebAssembly 例如 WebAssembly、WebAssembly.Module等 (14)其他 例如 arguments
详细资料可参考:JavaScript 标准内置对象
已经在作用域中声明但还没有赋值的变量,是undefined;
还没有在作用域声明的变量,是undeclared
对于undeclared变量的引用,浏览器会报错误,如Uncaught ReferenceError: b is not defined
。但是我们可以通过使用typeof来防止这个报错,因为对于undeclared的变量来说,typeof会返回‘undefined’。
首先null和undefined两者都是基本数据类型,并且这两个的数据类型分别只有一个值,就是null和undefined。
undefined代表的含义是未定义,而null代表的含义是空对象。在一般情况下,变量声明但还没有赋值的时候就会返回undefined,null主要是赋值给一些可能会返回一个对象的变量,作为初始化。
在JS中undefined并不是一个保留字,这说明我们可以使用undefined来作为一个变量,但是这样的做法是非常危险的,它会影响到我们对undefined值的判断。但是我们可以通过一些方法来获取安全的undefined值,例如 void 0
当我们使用typeof对这两个类型进行类型判断时,null会返回"object",这是一个历史遗留问题。因为在最初的JS的实现中,JS中的值是由一个表示类型的标签和实际数据值表示的。而对象的类型标签是0,由于null代表的是空指针(大多数平台下是0x00),因此,null的标签类型也就成为了0,故typeof null 就错误的返回了"object"。
当我们使用 == 来进行两者的比较时会返回true,而使用 === 时会返回false
{} 的 valueOf 结果为 {} ,toString 的结果为 "[object Object]"
[] 的 valueOf 结果为 [] ,toString 的结果为 ""
var arr = []
var obj = {}
obj.valueOf() //{}
obj.toString() //"[object Object]"
arr.valueOf() //[]
arr.toString() //""
将基本数据类型转化为对应的引用数据类型的操作
例:
var s1 = "abc";
var s2 = s1.indexOf("a")
此时我们声明的s1是一个基本类型值,它并不是一个对象,当然也不该有方法。但是js内部为我们完成了一系列的处理(即装箱),使得它能够调用方法,实现的机制如下:
代码实现:
var s1 = new String("some text");
var s2 = s1.substring(2);
s1 = null;
例子:
let q = '你好'
console.log(q,q.length,q.valueOf()) //你好 2 你好
let w =new String('你好')
console.log(w,w.length,w.valueOf()) //String{'你好'} 2 你好
将引用数据类型转换成对应的基本数据类型
它主要是通过引用类型的valueof()或者toString()方法来实现的。
例:
let a =new String('你好')
let b = a.toString()
console.log(b) //你好
在开发过程中有时候会遇到这样的情况:
let a = 0.1
let b = 0.2
console.log(a + b); // 0.30000000000000004
console.log(a + b === 0.3); // false
因为计算机是以二进制的方式来存储数据的,所以当计算机计算0.1+0.2时,会将其转换成二进制的形式再进行运算。但又因为0.1和0.2是无限循环的小数,当它们相加之后的值也是一个无限循环的小数,而在二进制科学表示法中,双精度浮点数的小数部分只能保留52位,剩余的都将要舍去,并且要遵循“0舍1入”的原则。
根据这样的原则,0.1和0.2的二进制相加,再转换成十进制就是:0.30000000000000004
toFixed(num)
方法可以将数字四舍五入为指定小数位数的数字。let a = 0.1
let b = 0.2
console.log((a + b).toFixed(2)); // 0.30
定义一个方法
function add(a, b) {
m = Math.pow(10, 2); // 将其放大10*2倍
return (a * m + b * m) / m; // 计算相加结果再将其放小10*2倍
}
console.log(add(0.1, 0.2));
==
")进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。===
")进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。Object.is()
来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。console.log(-0 == +0); // true
console.log(-0 === +0); // false
console.log(Object.is(-0, +0)); // true
console.log(NaN == NaN); // false
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true
事件是用户操作网页时发生的交互动作或者网页本身的一些操作,现代浏览器一共有三种事件模型。
其有两种实现方式:
<button onclick="click()">点我</button>
const btn = document.getElementById('btn')
btn.onclick = function(){
//do something
}
//解除事件
btn.onclick = null
缺点:通过DOM0事件模型来绑定事件,一个dom节点只能绑定一个事件,再次绑定其他事件将会覆盖原先绑定的事件。
DOM2新增了冒泡和捕获的概念,并且支持一个dom元素节点可以绑定多个事件
//监听事件 addEventListener(eventType, listener, useCapture) //移除事件 removeEventListener(eventType, listener, useCapture) //eventType(必需):事件名,支持所有的DOM事件 //listener(必需):指定当事件触发时所要执行的函数 //useCapture(可选):指定事件是否在捕获还是冒泡阶段执行;true表示捕获;false表示冒泡;默认为false var box1 = document.getElementById('box1'); var box2 = document.getElementById("box2"); box1.addEventListener("click", function() { console.log("box1") },false) box2.addEventListener("click",function() { console.log("box2") },false)
IE事件只支持冒泡,所以事件流只有两个阶段:
// 绑定事件
el.attachEvent(eventType, listener)
// 移除事件
el.detachEvent(eventType, listener)
当一个dom元素发生事件时,事件并不是完全发生在该元素上面,事件传播有三个阶段;
当事件发生在DOM元素上,该事件并不完全发生在该元素上。在捕获阶段,事件从window开始,一直到触发事件的元素。window---->document---->html---->body---->目标元素
var father = document.querySelector(".father");
var son = document.querySelector(".son");
// 捕获阶段,如果addEventListener 第三个参数是true,那么此时处于捕获阶段 window->document->html->body->father->son
son.addEventListener("click", function () {
alert("son盒子被点击了");
}, true);
father.addEventListener("click", function () {
alert("father盒子被点击了");
}, true);
document.addEventListener('click', function () {
alert("document被点击了")
}, true)
window.addEventListener('click', function () {
alert('window被点击了')
}, true)
事件冒泡与事件捕获相反,当前元素---->body---->html---->document---->window
var father = document.querySelector(".father");
var son = document.querySelector(".son");
// 冒泡阶段,如果addEventListener 第三个参数是false或者省略,那么此时处于冒泡阶段 son->father->body->html->document->window
son.addEventListener("click", function () {
alert("son盒子被点击了");
}, false);
father.addEventListener("click", function () {
alert("father盒子被点击了");
}, false);
document.addEventListener('click', function () {
alert("document被点击了")
})
window.addEventListener('click', function () {
alert('window被点击了')
})
事件委托的本质就是利用了浏览器事件冒泡的机制。由于事件在冒泡阶段会上传到父节点,并且父节点可以通过事件对象获取到目标节点,因此可以将子节点的监听函数定义在父节点上面,由父节点来统一处理多个子节点的事件,这种方式就叫做事件委托(事件代理)
优点:
若有一个列表,里面有大量的列表项,我们需要点击列表项的时候响应一个事件;此时如果给每一个列表项绑定事件的话,那对内存的消耗是极大的,因此更好的方法就是将这个事件绑定在它们的父元素上,然后执行事件的时候再去匹配判断目标元素。
如果动态添加或者删除了列表项的元素,那么每一次更改的时候就要重新给新增的元素绑定事件,给删除的元素解绑事件。此时使用事件委托就没有这样的麻烦,事件绑定在父元素和是哪个,与目标元素的增删是没有关系的,这样可以减少很多重复的工作
例:
像这样有很多的li元素,我们就可以采用事件委托来给父元素ul绑定事件,然后点击子元素来触发事件
<ul>
<li>这是li1</li>
<li>这是li2</li>
<li>这是li3</li>
<li>这是li4</li>
<li>这是li5</li>
<li>这是li6</li>
</ul>
var ul = document.querySelector('ul');
ul.addEventListener('click', function (e) {
e.target.style.backgroundColor = 'red';
});
提升:是指将变量和函数的声明提升到其作用域的最顶部;
JS在执行代码时会有两个阶段:编译和执行。
编译:在此阶段,JS引擎会获取所有的函数声明和变量声明,将它们提升到其对应的作用域的最顶端,并给它们赋值为undefined;(函数的声明比变量的声明更置顶;声明过的变量不会重复声明,但赋值会覆盖掉之前的声明)
执行:在此阶段,JS会将值赋给之前提升的变量并且调用函数
变量声明的提升是以将变量提升到其当前的作用域的最顶端,即全局作用域中声明的变量会提升至全局的最顶层,函数内声明的变量只会提升到该函数作用域的最顶层。
例:
console.log(a); var a = "a"; var foo = function() { console.log(a); var a = "a1"; } foo(); //编译后的代码为 var a; var foo; console.log(a); a = 'a' foo = function () { var a; console.log(a); a = 'a1' } foo();
ES6中新增的let和const关键字,由它们声明的变量和函数是没有提升的。
函数分为函数表达式和函数声明,这两者的提升是有区别的。
例:
console.log(foo1); // [Function: foo1] foo1(); // foo1 console.log(foo2); // undefined foo2(); // TypeError: foo2 is not a function function foo1 () { console.log("foo1"); }; var foo2 = function () { console.log("foo2"); }; //编译之后 function foo1() { console.log("foo1"); }; var foo2; console.log(foo1); foo1(); console.log(foo2); foo2(); foo2 = function () { console.log("foo2"); };
即函数提升只会提升函数声明,而不会提升函数表达式;
例:
var a = 1; function foo() { a = 10; console.log(a); return; function a() {}; } foo(); console.log(a); //编译之后 var a; //定义一个全局变量a function foo() { function a() { }; //函数声明提升到当前作用域的顶部 a = 10; //修改局部变量a的值 console.log(a); // 10 return; } a = 1; foo(); console.log(a); // 1
主要的有两个原因:
(1)提高性能,在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做的原因就是为了提高性能,如果没有这一步,那么每次执行代码前就要必须重新解析一次该变量或者函数,而这样是没有必要的,因为变量和函数的代码并不会改变,因此解析一次就足够了。
(2)容错性更好,在有时开发阶段可能会因为代码复杂而疏忽了先使后定义,这样也不会有报错。由于变量提升的存在,而会正常的运行。
类数组对象就是一个拥有length属性和若干索引值的对象,类数组与数组相似,但是不能调用数组中的方法。
常见的类数组对象有argument和DOM方法的返回结果。
首先得到一个类数组对象
//得到一个类数组对象
function lis() {
console.log(arguments); //arguments是一个类数组对象
}
lis(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
var argumentSlice = Array.prototype.slice.call(arguments)
var argumentSplice = Array.prototype.splice.call(arguments, 0)
var argumentsApply = Array.prototype.concat.apply([], arguments);
var argumentsFrom = Array.from(arguments);
-ES6扩展运算符
var argumentsList = [...arguments];
无论是否为严格模式,this都指向window;
console.log(this); //Window {window: Window, self: Window, document: document, name: '', location: Location, …}
或
"use strict";
console.log(this); //Window {window: Window, self: Window, document: document, name: '', location: Location, …}
window.test()
,否则光一个test(),它的this就是undefined;//非严格模式下
function This() {
console.log(this); //Window {window: Window, self: Window, document: document, name: '', location: Location, …}
}
This();
//严格模式下
function This() {
'use strict';
console.log(this); //undefined
}
This();
谁调用this就指向谁,若有多层嵌套的话,内部的this则指向最近的那个对象;
var obj = {
name: '张三',
age: 18,
say: function () {
console.log(this); //{name: '张三', age: 18, say: ƒ}
}
};
obj.say();
//若
window.obj.say() //this还是 {name: '张三', age: 18, say: ƒ}
箭头函数本身并没有this,其内部的this会绑定到最近的非箭头函数作用域中的this;
var say = () => {
console.log(this);
}
say(); // Window {window: Window, self: Window, document: document, name: '', location: Location, …}
这个this就指向其实例化的对象
function Person(name, age) {
this.name = name;
this.age = age;
this.say = function () {
console.log(this); //Person {name: '张三', age: 18, say: ƒ}
}
}
var person = new Person('张三', 18);
person.say();
总结:看谁调用则this就指向谁
这三个方法都可以指定函数调用时this的指向
call()方法会接收两个参数:一个是this绑定的对象,一个是传递的参数,而这个参数需要挨个列举出来;
例
var person = {
name: ""
};
setName.call(person,"xiao","ming"); //将setName中的this绑定到person上
console.log(person.name); // "xiao ming"
function setName(firstName,lastName){
this.name = firstName + lastName;
}
apply()方法与call()方法的作用相同,它们的区别主要是传递参数的方式有所不同,apply()方法传递的参数需要是以数组形式来传递
例:
var person = {
name: ""
};
setName.apply(person,["xiao","ming"]); //同样将setName中的this绑定到person上
console.log(person.name); // "xiao ming"
function setName(firstName,lastName){
this.name = firstName + lastName;
}
bind()方法和call()方法使用方式相同,但是call()方法绑定了this后会立刻执行,而bind()方法绑定了this之后,不会立刻执行;同时传递的参数可以一次性传递也可以挨个传递;
var person = { name: 'i', says: function (act, obj) { console.log(`${this.name} ${act} ${obj}`); } }; person.says('love', 'you'); // I love you // call会立即执行,bind并不会立即执行 person.says.call(person, 'love', 'you-call'); // I love you // 一次性传递参数 love = person.says.bind(person, 'love', 'you-bindAll'); love(); // I love you // 挨个传递参数 byvoidLoves = person.says.bind(person, 'love-bindByoneBy'); byvoidLoves('you'); // I love you // 另一种写法 otherbyvoidLoves = person.says.bind(person, 'love')('youOther'); // I love you1
JS引擎在执行一段可执行代码之前会进行准备工作,也就是对这段代码的解析(也可以成为预处理)。在这个阶段会根据可执行代码创建出其相对应的执行上下文,然后在代码解析完成后才开始代码的执行。
可执行代码分为三种:
执行上下文定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行上下文都有以下三个属性组成:
JS引擎会使用执行上下文栈(Execution context stack,ECS)来管理执行上下文
当JS执行代码时,首先遇到全局代码,会创建一个全局执行上下文并将其压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个函数执行上下文并压入栈顶;在执行时,引擎会先执行位于执行上下文栈中栈顶的函数,当该函数执行完成之后,就会从执行上下文栈中弹出,接着进行执行下一个执行上下文;以此执行,当全部的代码执行完毕之后,就会从栈中弹出全局执行上下文。
例:
var a = "global var"; function foo(){ console.log(a); } function outerFunc(){ var b = "var in outerFunc"; console.log(b); function innerFunc(){ var c = "var in innerFunc"; console.log(c); foo(); } innerFunc(); } outerFunc();
代码首先进入Global Execution Context,然后依次进入outerFunc,innerFunc和foo的执行上下文,执行上下文栈就可以表示为:
作用域:它是定义变量的区域,可分为全局作用域和局部作用域
一般情况下,变量的取值会从创建这个变量的作用域中取值;但是如果当前的作用域中没有查到该值,就会向上级作用域去查找,直到查到全局作用域,这样的一个查找过程形成的链条就叫做作用域链。
闭包是指有权访问另一个函数作用域内变量的函数。
创建闭包的最常见的方式就是在函数内部再创建一个函数;新创建的函数就可以访问外部函数的局部变量。
例:
// 写一个闭包
function a() {
var n = 0;
function add() {
n++;
console.log(n);
}
return add;
}
var a1 = a(); //注意,函数名只是一个标识(指向函数的指针),而()才是执行函数;
a1(); //1
a1(); //2 第二次调用n变量还在内存中
练习题:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());
//object.getNameFunc() 这个的时候this是指向object的
//object.getNameFun()() 这个时候this就指向了window
//所以结果就是全局下的name,The window
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()());
//这个看似与上面差不多,
//但是var that = this 这个操作保存了当前的this指向object,
//所以后面的that都指的是object,故结果为 My Object
闭包的优点:
闭包的缺点:
document
。window
。widow对象还含有很多的子对象,如location
对象、navigator
对象、screen
对象等,并且DOM最根本的对象document
对象也是BOM的window
对象的子对象。浅拷贝是创建一个对象,这个对象跟原来对象有着一样的属性值,并且这两个对象的指针指向堆内存的地址是一致的。所以当这两个对象中的其中一个对象改变了,那么也会影响到另一个对象。
// 定义一个对象 let people = { name: '张三', age: 18, } // 进行浅拷贝 let newPeople = people // 输出原对象和新对象的值 console.log('原对象', people); console.log('新对象', newPeople); // 修改原对象的值 people.name = '李四' // 输出原对象和新对象的值 console.log('修改后的原对象', people); console.log('修改后的新对象', newPeople);
let people = { name: '张三', age: 18, address: ['北京', '上海', '广州'], school: { name: '清华大学', address: '北京' }, say: function () { console.log('我是' + this.name); } } // 1.直接赋值的方式 let newPeople = people // 2.遍历对象的方式 //function clone(obj) { // let cloneObj = {} // for (const i in obj) { // cloneObj[i] = obj[i] // } // return cloneObj // } // let newPeople = clone(people) // 3.Object.assign()方法 // let newPeople = Object.assign({}, people) // 4.lodash的cloneDeep方法 // let newPeople = _.clone(people) console.log(people, newPeople);
深拷贝就是在堆内存中新开辟一个内存空间,将拷贝的出来的新对象放在该内存空间中,并且修改新对象不会影响到原来的对象。
// 定义一个对象 let people = { name: '张三', age: 18 } // 进行深拷贝 let newPeople = JSON.parse(JSON.stringify(people)) // 输出原对象和新对象的值 console.log(people); console.log(newPeople); // 修改原对象的值 people.name = '李四' console.log('修改后原对象的值', people); console.log('修改后新对象的值', newPeople);
let people = { name: '张三', age: 18, address: ['北京', '上海', '广州'], school: { name: '清华大学', address: '北京' }, say: function () { console.log('我是' + this.name); } } //1.通过遍历来实现 function Deep(startObj, endObj) { var obj = endObj || {} for (let i in startObj) { if (typeof startObj[i] === 'object') { obj[i] = startObj[i].constructor === Array ? [] : {} Deep(startObj[i], obj[i]) } else { obj[i] = startObj[i] } } return obj } let newPeople = Deep(people) // 2.JSON.parse(JSON.stringify()) // let newPeople = JSON.parse(JSON.stringify(people)) // 3.lodash中的cloneDeep()方法 // let newPeople = _.cloneDeep(people) people.name = '李四' console.log(people, newPeople);
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。