赞
踩
基本数据类型:
undefined
: 表示未定义或未初始化的值。
null
: 表示空值或不存在的对象。
boolean
: 表示逻辑上的true
或false
。
number
: 表示数值,包括整数和浮点数。
string
: 表示字符串。
symbol
: 表示唯一的、不可变的值,通常用作对象的属性键。
引用数据类型:
object
: 表示一个复杂的数据结构,可以包含多个键值对。
array
: 表示一个有序的、可变长度的集合。
function
: 表示可执行的代码块,可以被调用执行。
基本数据类型在赋值时是按值传递的,每个变量都有自己的存储空间,修改一个变量不会影响其他变量。而引用数据类型在赋值时是按引用传递的,多个变量引用同一个对象,修改一个变量会影响其他变量。需要注意的是,null
和undefined
既是基本数据类型,也是特殊的值,表示不同的含义。
JavaScript中有许多内置对象,用于提供各种功能和方法,常见的内置对象包括:
Object
: 所有对象的基类。
Array
: 用于表示和操作数组的对象。
Boolean
: 代表布尔值 true
或 false
。
Number
: 代表数字,用于执行数值操作和计算。
String
: 代表字符串,用于处理和操作文本数据。
Date
: 用于处理日期和时间。
RegExp
: 用于进行正则表达式匹配。
Function
: 用于定义和调用函数。
Math
: 提供数学计算相关的方法和常量。
JSON
: 用于解析和序列化 JSON 数据。
Error
: 用于表示和处理错误。
Map
: 一种键值对的集合,其中键可以是任意类型。
Set
: 一种集合数据结构,存储唯一的值。
Promise
: 用于处理异步操作和编写更优雅的异步代码。
Symbol
: 代表唯一的标识符。
这些内置对象提供了丰富的功能和方法,可以满足不同的编程需求。开发人员可以利用这些对象来处理数据、执行操作、处理错误等。
•闭包就是能够读取其他函数内部变量的函数
•闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量,利用闭包可以突破作用链域
•闭包的特性:函数内再嵌套函数内部函数可以引用外层的参数和变量参数和变量不会被垃圾回收机制回收
说说你对闭包的理解
•使用闭包主要是为了设计私有的方法和变量。闭包的优点是可以避免全局变量的污染,缺点是闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。在js中,函数即闭包,只有函数才会产生作用域的概念
•闭包 的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中
•闭包的另一个用处,是封装对象的私有属性和私有方法
•好处:能够实现封装和缓存等;
•坏处:就是消耗内存、不正当使用会造成内存溢出的问题
使用闭包的注意点
•由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露
•解决方法是,在退出函数之前,将不使用的局部变量全部删除\
•常用场景就是防抖节流
事件流分为三个阶段:捕获阶段、目标阶段和冒泡阶段。
1.捕获阶段(Capture Phase):事件从最外层的父节点开始向下传递,直到达到目标元素的父节点。在捕获阶段,事件会经过父节点、祖父节点等,但不会触发任何事件处理程序。
2.目标阶段(Target Phase):事件到达目标元素本身,触发目标元素上的事件处理程序。如果事件有多个处理程序绑定在目标元素上,它们会按照添加的顺序依次执行。
3.冒泡阶段(Bubble Phase):事件从目标元素开始向上冒泡,传递到父节点,直到传递到最外层的父节点或根节点。在冒泡阶段,事件会依次触发父节点、祖父节点等的事件处理程序。
事件流的默认顺序是从目标元素的最外层父节点开始的捕获阶段,然后是目标阶段,最后是冒泡阶段。但是可以通过事件处理程序的绑定顺序来改变事件处理的执行顺序。
- <div id="outer">
- <div id="inner">
- <button id="btn">Click me</button>
- </div>
- </div>
- let outer = document.getElementById('outer');
- let inner = document.getElementById('inner');
- let btn = document.getElementById('btn');
-
- outer.addEventListener('click', function() {
- console.log('Outer div clicked');
- }, true); // 使用捕获阶段进行事件监听
-
- inner.addEventListener('click', function() {
- console.log('Inner div clicked');
- }, false); // 使用冒泡阶段进行事件监听
-
- btn.addEventListener('click', function() {
- console.log('Button clicked');
- }, false); // 使用冒泡阶段进行事件监听
-
当点击按钮时,事件的执行顺序如下:
1.捕获阶段:触发外层div的捕获事件处理程序。
2.目标阶段:触发按钮的事件处理程序。
3.冒泡阶段:触发内层div的冒泡事件处理程序。
- Outer div clicked
- Button clicked
- Inner div clicked
这个示例展示了事件流中捕获阶段、目标阶段和冒泡阶段的执行顺序。
可以通过addEventListener方法的第三个参数来控制事件处理函数在捕获阶段或冒泡阶段执行,true表示捕获阶段,false或不传表示冒泡阶段
new 关键词的主要作用就是执行一个构造函数、返回一个实例对象,在 new 的过程中,根据构造函数的情况,来确定是否可以接受参数的传递。下面我们通过一段代码来看一个简单的 new 的例子
- function Person(){
- this.name = 'Jack';
- }
- var p = new Person();
- console.log(p.name) // Jack
这段代码比较容易理解,从输出结果可以看出,p 是一个通过 person 这个构造函数生成的一个实例对象,这个应该很容易理解
new 操作符可以帮助我们构建出一个实例,并且绑定上 this,内部执行步骤可大概分为以下几步:
•创建一个新对象
•对象连接到构造函数原型上,并绑定 this
(this 指向新对象)
•执行构造函数代码(为这个新对象添加属性)
•返回新对象
在第四步返回新对象这边有一个情况会例外:
- function Person(){
- this.name = 'Jack';
- }
- var p = Person();
- console.log(p) // undefined
- console.log(name) // Jack
- console.log(p.name) // 'name' of undefined
•从上面的代码中可以看到,我们没有使用 new
这个关键词,返回的结果就是 undefined
。其
中由于 JavaScript
代码在默认情况下 this
的指向是 window
,那么 name
的输出结果就为
Jack
,这是一种不存在 new
关键词的情况。
•那么当构造函数中有 return
一个对象的操作,结果又会是什么样子呢?我们再来看一段在上面的基础上改造过的代码。
- function Person(){
- this.name = 'Jack';
- return {age: 18}
- }
- var p = new Person();
- console.log(p) // {age: 18}
- console.log(p.name) // undefined
- console.log(p.age) // 18
通过这段代码又可以看出,当构造函数最后 return 出来的是一个和 this 无关的对象时,new 命令会直接返回这个新对象,而不是通过 new 执行步骤生成的 this 对象
但是这里要求构造函数必须是返回一个对象,如果返回的不是对象,那么还是会按照 new 的实现步骤,返回新生成的对象。接下来还是在上面这段代码的基础之上稍微改动一下
- function Person(){
- this.name = 'Jack';
- return 'tom';
- }
- var p = new Person();
- console.log(p) // {name: 'Jack'}
- console.log(p.name) // Jack
可以看出,当构造函数中 return 的不是一个对象时,那么它还是会根据 new 关键词的执行逻辑,生成一个新的对象(绑定了最新 this),最后返回出来
因此我们总结一下:new 关键词执行之后总是会返回一个对象,要么是实例对象,要么是 return 语句指定的对象
-
- 手工实现New的过程
- function create(fn, ...args) {
- if(typeof fn !== 'function') {
- throw 'fn must be a function';
- }
- // 1、用new Object() 的方式新建了一个对象obj
- // var obj = new Object()
- // 2、给该对象的__proto__赋值为fn.prototype,即设置原型链
- // obj.__proto__ = fn.prototype
-
- // 1、2步骤合并
- // 创建一个空对象,且这个空对象继承构造函数的 prototype 属性
- // 即实现 obj.__proto__ === constructor.prototype
- var obj = Object.create(fn.prototype);
-
- // 3、执行fn,并将obj作为内部this。使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性
- var res = fn.apply(obj, args);
- // 4、如果fn有返回值,则将其作为new操作返回内容,否则返回obj
- return res instanceof Object ? res : obj;
- };
•使用 Object.create
将 obj 的
proto指向为构造函数的原型
;
•使用 apply
方法,将构造函数内的 this
指向为 obj
;
•在 create
返回时,使用三目运算符决定返回结果。
我们知道,构造函数如果有显式返回值,且返回值为对象类型,那么构造函数返回结果不再是目标实例
- function Person(name) {
- this.name = name
- return {1: 1}
- }
- const person = new Person(Person, 'lucas')
- console.log(person)
- // {1: 1}
- //使用create代替new
- function Person() {...}
- // 使用内置函数new
- var person = new Person(1,2)
-
- // 使用手写的new,即create
- var person = create(Person, 1,2)
new 被调用后大致做了哪几件事情
•让实例可以访问到私有属性;
•让实例可以访问构造函数原型(constructor.prototype
)所在原型链上的属性;
•构造函数返回的最后结果是引用数据类型。
__proto__和prototype关系
:__proto__和constructor是对象独有的。prototype属性是函数独有的
在 js 中我们是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性值,这个属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当我们使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说我们是不应该能够获取到这个值的,但是现在浏览器中都实现了 proto 属性来让我们访问这个属性,但是我们最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,我们可以通过这个方法来获取对象的原型。
当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype
所以这就是我们新建的对象为什么能够使用 toString()
等方法的原因。
特点:JavaScript 对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与 之相关的对象也会继承这一改变
•原型(prototype
): 一个简单的对象,用于实现对象的 属性继承。可以简单的理解成对象的爹。在 Firefox
和 Chrome
中,每个JavaScript
对象中都包含一个__proto__
(非标准)的属性指向它爹(该对象的原型),可obj.__proto__
进行访问。
•构造函数: 可以通过new
来 新建一个对象 的函数。
•实例: 通过构造函数和new
创建出来的对象,便是实例。 实例通过__proto__
指向原型,通过constructor
指向构造函数。
以Object为例,我们常用的Object便是一个构造函数,因此我们可以通过它构建实例。
- // 实例
- const instance = new Object()
则此时, 实例为instance, 构造函数为Object,我们知道,构造函数拥有一个prototype的属性指向原型,因此原型为:
- // 原型
- const prototype = Object.prototype
这里我们可以来看出三者的关系:
•实例.__proto__ === 原型
•原型.constructor === 构造函数
•构造函数.prototype === 原型
- // 这条线其实是是基于原型进行获取的,可以理解成一条基于原型的映射线
- // 例如:
- // const o = new Object()
- // o.constructor === Object --> true
- // o.__proto__ = null;
- // o.constructor === Object --> false
- 实例.constructor === 构造函数
原型链
原型链是由原型对象组成,每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型,__proto__ 将对象连接起来组成了原型链。是一个用来实现继承和共享属性的有限的对象链
•属性查找机制: 当查找对象的属性时,如果实例对象自身不存在该属性,则沿着原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查找,直至最顶级的原型对象Object.prototype
,如还是没找到,则输出undefined
;
•属性修改机制: 只会修改实例对象本身的属性,如果不存在,则进行添加该属性,如果需要修改原型的属性时,则可以用: b.prototype.x = 2
;但是这样会造成所有继承于该对象的实例的属性发生改变。
js 获取原型的方法
•p.proto
•p.constructor.prototype
•Object.getPrototypeOf(p)
总结
•每个函数都有 prototype
属性,除了 Function.prototype.bind()
,该属性指向原型。
•每个对象都有 __proto__
属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]]
,但是 [[prototype]]
是内部属性,我们并不能访问到,所以使用 _proto_
来访问。
•对象可以通过 __proto__
来寻找不属于该对象的属性,__proto__
将对象连接起来组成了原型链。
涉及面试题:原型如何实现继承?Class 如何实现继承?Class 本质是什么?
首先先来看下 class
,其实在 JS
中并不存在类,class
只是语法糖,本质还是函数
- class Person {}
- Person instanceof Function // true
-
组合继承
组合继承是最常用的继承方式
- 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, {
- constructor: {
- value: Child,
- enumerable: false,
- writable: true,
- configurable: true
- }
- })
-
- const child = new Child(1)
-
- child.getValue() // 1
- child instanceof Parent // true
以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。
Class 继承
以上两种继承方式都是通过原型去解决的,在 ES6 中,我们可以使用 class 去实现继承,并且实现起来很简单
- class Parent {
- constructor(value) {
- this.val = value
- }
- getValue() {
- console.log(this.val)
- }
- }
- class Child extends Parent {
- constructor(value) {
- super(value)
- this.val = value
- }
- }
- let child = new Child(1)
- child.getValue() // 1
- child instanceof Parent // true
class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)。
ES5 和 ES6 继承的区别:
•ES6 继承的子类需要调用 super()
才能拿到子类,ES5 的话是通过 apply
这种绑定的方式
•类声明不会提升,和 let
这些一致
- function Super() {}
- Super.prototype.getNumber = function() {
- return 1
- }
-
- function Sub() {}
- Sub.prototype = Object.create(Super.prototype, {
- constructor: {
- value: Sub,
- enumerable: false,
- writable: true,
- configurable: true
- }
- })
- let s = new Sub()
- s.getNumber()
几种常见的继承方式
1. 方式1: 借助call
-
- function Parent1(){
- this.name = 'parent1';
- }
- function Child1(){
- Parent1.call(this);
- this.type = 'child1'
- }
- console.log(new Child1);
这样写的时候子类虽然能够拿到父类的属性值,但是问题是父类原型对象中一旦存在方法那么子类无法继承。那么引出下面的方法。
2. 方式2: 借助原型链
- function Parent2() {
- this.name = 'parent2';
- this.play = [1, 2, 3]
- }
- function Child2() {
- this.type = 'child2';
- }
- Child2.prototype = new Parent2();
-
- console.log(new Child2());
-
看似没有问题,父类的方法和属性都能够访问,但实际上有一个潜在的不足。举个例子:
- var s1 = new Child2();
- var s2 = new Child2();
- s1.play.push(4);
- console.log(s1.play, s2.play);
- [1,2,3,4] [1,2,3,4]
可以看到控制台只改变了s1的play属性,为什么s2也跟着变了呢?很简单,因为两个实例使用的是同一个原型对象。
3. 方式3:将前两种组合
- function Parent3 () {
- this.name = 'parent3';
- this.play = [1, 2, 3];
- }
- function Child3() {
- Parent3.call(this);
- this.type = 'child3';
- }
- Child3.prototype = new Parent3();
- var s3 = new Child3();
- var s4 = new Child3();
- s3.play.push(4);
- console.log(s3.play, s4.play);
- [1,2,3,4] [1,2,3]
可以看到控制台:
之前的问题都得以解决。但是这里又徒增了一个新问题,那就是Parent3的构造函数会多执行了一次(Child3.prototype = new Parent3();)。这是我们不愿看到的。那么如何解决这个问题?
4. 方式4: 组合继承的优化1
- function Parent4 () {
- this.name = 'parent4';
- this.play = [1, 2, 3];
- }
- function Child4() {
- Parent4.call(this);
- this.type = 'child4';
- }
- Child4.prototype = Parent4.prototype;
-
这里让将父类原型对象直接给到子类,父类构造函数只执行一次,而且父类属性和方法均能访问,但是我们来测试一下:
- var s3 = new Child4();
- var s4 = new Child4();
- console.log(s3)
5. 方式5(最推荐使用): 组合继承的优化2
- function Parent5 () {
- this.name = 'parent5';
- this.play = [1, 2, 3];
- }
- function Child5() {
- Parent5.call(this);
- this.type = 'child5';
- }
- Child5.prototype = Object.create(Parent5.prototype);
- Child5.prototype.constructor = Child5;
-
这是最推荐的一种方式,接近完美的继承,它的名字也叫做寄生组合继承。
6. ES6的extends被编译后的JavaScript代码
ES6的代码最后都是要在浏览器上能够跑起来的,这中间就利用了babel这个编译工具,将ES6的代码编译成ES5让一些不支持新语法的浏览器也能运行。
那最后编译成了什么样子呢?
- function _possibleConstructorReturn(self, call) {
- // ...
- return call && (typeof call === 'object' || typeof call === 'function') ? call : self;
- }
-
- function _inherits(subClass, superClass) {
- // ...
- //看到没有
- subClass.prototype = Object.create(superClass && superClass.prototype, {
- constructor: {
- value: subClass,
- enumerable: false,
- writable: true,
- configurable: true
- }
- });
- if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
- }
-
-
- var Parent = function Parent() {
- // 验证是否是 Parent 构造出来的 this
- _classCallCheck(this, Parent);
- };
-
- var Child = (function (_Parent) {
- _inherits(Child, _Parent);
-
- function Child() {
- _classCallCheck(this, Child);
-
- return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
- }
-
- return Child;
- }(Parent));
-
核心是_inherits函数,可以看到它采用的依然也是第五种方式————寄生组合继承方式,同时证明了这种方式的成功。不过这里加了一个Object.setPrototypeOf(subClass, superClass),这是用来干啥的呢?
答案是用来继承父类的静态方法。这也是原来的继承方式疏忽掉的地方。
追问: 面向对象的设计一定是好的设计吗?
不一定。从继承的角度说,这一设计是存在巨大隐患的。
JavaScript 内存泄露指对象在不需要使用它时仍然存在,导致占用的内存不能使用或回收
•未使用 var
声明的全局变量
•闭包函数(Closures
)
•循环引用(两个对象相互引用)
•控制台日志(console.log
)
•移除存在绑定事件的DOM
元素(IE
)
•setTimeout
的第一个参数使用字符串而非函数的话,会引发内存泄漏
•垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用数量为 0
(没有其他对象引用过该对象),或对该对象的惟一引用是循环的,那么该对象的内存即可回收
下面是一些常见操作可能导致内存泄漏的示例代码:
- 未使用 var 声明的全局变量:
- function foo() {
- bar = 'global variable'; // 没有使用 var 声明
- }
- 闭包函数(Closures):
- function outer() {
- var data = 'sensitive data';
- return function() {
- // 内部函数形成了闭包
- console.log(data);
- };
- }
- var inner = outer();
- inner(); // 闭包引用了外部函数的变量,导致变量无法被释放
-
- 循环引用:
- function createObjects() {
- var obj1 = {};
- var obj2 = {};
- obj1.ref = obj2;
- obj2.ref = obj1;
- // 对象之间形成循环引用,导致无法被垃圾回收
- }
- createObjects();
- 控制台日志(console.log):
- function processData(data) {
- console.log(data); // 控制台日志可能会引用数据,阻止垃圾回收
- // 处理数据的逻辑
- }
-
- 移除存在绑定事件的 DOM 元素(IE):
- var element = document.getElementById('myElement');
- element.onclick = function() {
- // 处理点击事件
- };
- // 移除元素时没有显式地解绑事件处理程序,可能导致内存泄漏(在 IE 浏览器中)
- element.parentNode.removeChild(element);
- 使用字符串作为 setTimeout 的第一个参数:
- setTimeout('console.log("timeout");', 1000);
- // 使用字符串作为参数,会导致内存泄漏(不推荐
•对于在JavaScript中的字符串,对象,数组是没有固定大小的,只有当对他们进行动态分配存储时,解释器就会分配内存来存储这些数据,当JavaScript的解释器消耗完系统中所有可用的内存时,就会造成系统崩溃。
•内存泄漏,在某些情况下,不再使用到的变量所占用内存没有及时释放,导致程序运行中,内存越占越大,极端情况下可以导致系统崩溃,服务器宕机。
•JavaScript有自己的一套垃圾回收机制,JavaScript的解释器可以检测到什么时候程序不再使用这个对象了(数据),就会把它所占用的内存释放掉。
•针对JavaScript的来及回收机制有以下两种方法(常用):标记清除,引用计数
•标记清除
v8 的垃圾回收机制基于分代回收机制,这个机制又基于世代假说,这个假说有两个特点,一是新生的对象容易早死,另一个是不死的对象会活得更久。基于这个假说,v8 引擎将内存分为了新生代和老生代。
•新创建的对象或者只经历过一次的垃圾回收的对象被称为新生代。经历过多次垃圾回收的对象被称为老生代。
•新生代被分为 From 和 To 两个空间,To 一般是闲置的。当 From 空间满了的时候会执行 Scavenge 算法进行垃圾回收。当我们执行垃圾回收算法的时候应用逻辑将会停止,等垃圾回收结束后再继续执行。
这个算法分为三步:
•首先检查 From 空间的存活对象,如果对象存活则判断对象是否满足晋升到老生代的条件,如果满足条件则晋升到老生代。如果不满足条件则移动 To 空间。
•如果对象不存活,则释放对象的空间。
•最后将 From 空间和 To 空间角色进行交换。
新生代对象晋升到老生代有两个条件:
•第一个是判断是对象否已经经过一次 Scavenge 回收。若经历过,则将对象从 From 空间复制到老生代中;若没有经历,则复制到 To 空间。
•第二个是 To 空间的内存使用占比是否超过限制。当对象从 From 空间复制到 To 空间时,若 To 空间使用超过 25%,则对象直接晋升到老生代中。设置 25% 的原因主要是因为算法结束后,两个空间结束后会交换位置,如果 To 空间的内存太小,会影响后续的内存分配。
老生代采用了标记清除法和标记压缩法。标记清除法首先会对内存中存活的对象进行标记,标记结束后清除掉那些没有标记的对象。由于标记清除后会造成很多的内存碎片,不便于后面的内存分配。所以了解决内存碎片的问题引入了标记压缩法。
由于在进行垃圾回收的时候会暂停应用的逻辑,对于新生代方法由于内存小,每次停顿的时间不会太长,但对于老生代来说每次垃圾回收的时间长,停顿会造成很大的影响。 为了解决这个问题 V8 引入了增量标记的方法,将一次停顿进行的过程分为了多步,每次执行完一小步就让运行逻辑执行一会,就这样交替运行
XML
(可扩展标记语言)和JSON
(JavaScript对象表示法)是两种常用的数据格式,它们在以下几个方面有一些区别:
数据体积方面:
•JSON
相对于XML来说,数据的体积小,因为JSON使用了较简洁的语法,所以传输的速度更快。
数据交互方面:
•JSON
与JavaScript的交互更加方便,因为JSON数据可以直接被JavaScript解析和处理,无需额外的转换步骤。
•XML
需要使用DOM操作来解析和处理数据,相对而言更复杂一些。
数据描述方面:
•XML
对数据的描述性较强,它使用标签来标识数据的结构和含义,可以自定义标签名,使数据更具有可读性和可扩展性。
•JSON
的描述性较弱,它使用简洁的键值对表示数据,适合于简单的数据结构和传递。
传输速度方面:
•JSON
的解析速度要快于XML
,因为JSON
的语法更接近JavaScript对象的表示,JavaScript引擎能够更高效地解析JSON数据。
需要根据具体的需求和使用场景选择合适的数据格式,一般来说,如果需要简单、轻量级的数据交互,并且与JavaScript紧密集成,可以选择JSON。而如果需要较强的数据描述性和扩展性,或者需要与其他系统进行数据交互,可以选择XML。
工厂模式(Factory Pattern):
•优点:封装了对象的创建过程,降低了耦合性,提供了灵活性和可扩展性。
•缺点:增加了代码的复杂性,需要创建工厂类。
•适用场景:当需要根据不同条件创建不同对象时,或者需要隐藏对象创建的细节时,可以使用工厂模式。
- class Button {
- constructor(text) {
- this.text = text;
- }
- render() {
- console.log(`Rendering button with text: ${this.text}`);
- }
- }
-
- class ButtonFactory {
- createButton(text) {
- return new Button(text);
- }
- }
-
- const factory = new ButtonFactory();
- const button = factory.createButton('Submit');
- button.render(); // Output: Rendering button with text: Submit
-
单例模式(Singleton Pattern)
•优点:确保一个类只有一个实例,节省系统资源,提供全局访问点。
•缺点:可能引入全局状态,不利于扩展和测试。
•适用场景:当需要全局唯一的对象实例时,例如日志记录器、全局配置对象等,可以使用单例模式。
- class Logger {
- constructor() {
- if (Logger.instance) {
- return Logger.instance;
- }
- Logger.instance = this;
- }
- log(message) {
- console.log(`Logging: ${message}`);
- }
- }
-
- const logger1 = new Logger();
- const logger2 = new Logger();
-
- console.log(logger1 === logger2); // Output: true
-
观察者模式(Observer Pattern):
•优点:实现了对象之间的松耦合,支持广播通信,当一个对象状态改变时,可以通知依赖它的其他对象进行更新。
•缺点:可能导致性能问题和内存泄漏,需要合理管理观察者列表。
•适用场景:当需要实现对象之间的一对多关系,一个对象的改变需要通知其他多个对象时,可以使用观察者模式。
- class Subject {
- constructor() {
- this.observers = [];
- }
- addObserver(observer) {
- this.observers.push(observer);
- }
- removeObserver(observer) {
- const index = this.observers.indexOf(observer);
- if (index !== -1) {
- this.observers.splice(index, 1);
- }
- }
- notify(message) {
- this.observers.forEach((observer) => observer.update(message));
- }
- }
-
- class Observer {
- update(message) {
- console.log(`Received message: ${message}`);
- }
- }
-
- const subject = new Subject();
- const observer1 = new Observer();
- const observer2 = new Observer();
-
- subject.addObserver(observer1);
- subject.addObserver(observer2);
- subject.notify('Hello, observers!'); // Output
-
发布订阅模式(Publish-Subscribe Pattern)
•优点:解耦了发布者和订阅者,使它们可以独立变化。增加了代码的灵活性和可维护性。
•缺点:可能会导致发布者过度发布消息,造成性能问题。订阅者需要订阅和取消订阅相关的逻辑。
•适用场景:当存在一对多的关系,一个对象的状态变化需要通知多个其他对象时,可以使用发布订阅模式。
- class PubSub {
- constructor() {
- this.subscribers = {};
- }
- subscribe(event, callback) {
- if (!this.subscribers[event]) {
- this.subscribers[event] = [];
- }
- this.subscribers[event].push(callback);
- }
- unsubscribe(event, callback) {
- const subscribers = this.subscribers[event];
- if (subscribers) {
- this.subscribers[event] = subscribers.filter(cb => cb !== callback);
- }
- }
- publish(event, data) {
- const subscribers = this.subscribers[event];
- if (subscribers) {
- subscribers.forEach(callback => callback(data));
- }
- }
- }
-
- // 创建发布订阅对象
- const pubsub = new PubSub();
-
- // 订阅事件
- const callback1 = data => console.log('Subscriber 1:', data);
- const callback2 = data => console.log('Subscriber 2:', data);
- pubsub.subscribe('event1', callback1);
- pubsub.subscribe('event1', callback2);
-
- // 发布事件
- pubsub.publish('event1', 'Hello, world!');
-
- // 取消订阅事件
- pubsub.unsubscribe('event1', callback2);
-
- // 再次发布事件
- pubsub.publish('event1', 'Hello again!');
-
在上述示例中,PubSub
是发布订阅的实现类,它维护一个订阅者列表 subscribers
,用于存储不同事件的订阅者列表。通过 subscribe
方法订阅事件,将回调函数添加到对应事件的订阅者列表中;通过 unsubscribe
方法取消订阅事件,从对应事件的订阅者列表中移除回调函数;通过 publish
方法发布事件,遍历对应事件的订阅者列表,依次执行回调函数。通过发布订阅模式,发布者和订阅者之间解耦,可以实现松散耦合的组件间通信。
发布订阅模式适用于许多场景,如事件驱动的系统、消息队列、UI组件间的通信等,可以实现组件之间的解耦和灵活性。
发布订阅模式(Publish-Subscribe Pattern)和观察者模式(Observer Pattern)是两种常见的设计模式,它们有一些相似之处,但也存在一些区别。
相似之处:
•都用于实现对象之间的消息通信和事件处理。
•都支持解耦,让发布者和订阅者(观察者)之间相互独立。
区别:
•关注点不同:观察者模式关注的是一个主题对象(被观察者)和多个观察者对象之间的关系。当主题对象的状态发生变化时,它会通知所有观察者对象进行更新。而发布订阅模式关注的是发布者和订阅者之间的关系,发布者将消息发送到一个中心调度器(或者称为事件总线),然后由调度器将消息分发给所有订阅者。
•中间件存在与否:发布订阅模式通常需要一个中间件(调度器或事件总线)来管理消息的发布和订阅,这样发布者和订阅者之间的通信通过中间件进行。而观察者模式则直接在主题对象和观察者对象之间进行通信,没有中间件的参与。
•松散耦合程度不同:观察者模式中,主题对象和观察者对象之间是直接关联的,主题对象需要知道每个观察者对象的存在。而在发布订阅模式中,发布者和订阅者之间并不直接关联,它们只与中间件进行通信,发布者和订阅者之间的耦合更加松散。
观察者模式示例:
- class Subject {
- constructor() {
- this.observers = [];
- }
- addObserver(observer) {
- this.observers.push(observer);
- }
- removeObserver(observer) {
- this.observers = this.observers.filter(obs => obs !== observer);
- }
- notify(data) {
- this.observers.forEach(observer => observer.update(data));
- }
- }
-
- class Observer {
- update(data) {
- console.log('Received data:', data);
- }
- }
-
- // 创建主题对象
- const subject = new Subject();
-
- // 创建观察者对象
- const observer1 = new Observer();
- const observer2 = new Observer();
-
- // 添加观察者
- subject.addObserver(observer1);
- subject.addObserver(observer2);
-
- // 发送通知
- subject.notify('Hello, observers!');
-
发布订阅模式示例:
- class EventBus {
- constructor() {
- this.subscribers = {};
- }
- subscribe(event, callback) {
- if (!this.subscribers[event]) {
- this.subscribers[event] = [];
- }
- this.subscribers[event].push(callback);
- }
- unsubscribe(event, callback) {
- const subscribers = this.subscribers[event];
- if (subscribers) {
- this.subscribers[event] = subscribers.filter(cb => cb !== callback);
- }
- }
- publish(event, data) {
- const subscribers = this.subscribers[event];
- if (subscribers) {
- subscribers.forEach(callback => callback(data));
- }
- }
- }
-
- // 创建事件总线对象
- const eventBus = new EventBus();
-
- // 订阅事件
- eventBus.subscribe('message', data => {
- console.log('Received message:', data);
- });
-
- // 发布事件
- eventBus.publish('message', 'Hello, subscribers!');
-
在上述示例中,观察者模式中的Subject类相当于发布订阅模式中的EventBus类,Observer类相当于订阅者(观察者),notify方法相当于publish方法,update方法相当于订阅者接收到事件后的回调函数。
观察者模式和发布订阅模式都是常见的用于实现事件处理和消息通信的设计模式,根据实际场景和需求选择合适的模式进行使用。观察者模式更加简单直接,适用于一对多的关系,而发布订阅模式更加灵活,可以支持多对多的关系,并且通过中间件来解耦发布者和订阅者。
原型模式(Prototype Pattern)
•优点:通过克隆现有对象来创建新对象,避免了频繁的对象创建过程,提高了性能。
•缺点:需要正确设置原型对象和克隆方法,可能引入深拷贝或浅拷贝的问题。
•适用场景:当创建对象的成本较大且对象之间相似度较高时,可以使用原型模式来复用已有对象。
- class Shape {
- constructor() {
- this.type = '';
- }
- clone() {
- return Object.create(this);
- }
- draw() {
- console.log(`Drawing a ${this.type}`);
- }
- }
-
- const circlePrototype = new Shape();
- circlePrototype.type = 'Circle';
-
- const squarePrototype = new Shape();
- squarePrototype.type = 'Square';
-
- const circle = circlePrototype.clone();
- circle.draw(); // Output: Drawing a Circle
-
- const square = squarePrototype.clone();
- square.draw(); // Output: Drawing a Square
-
装饰者模式(Decorator Pattern)
•优点:动态地给对象添加新的功能,避免了使用子类继承的方式导致类爆炸的问题。
•缺点:增加了代码的复杂性,需要理解和管理装饰器的层次结构。
•适用场景:当需要在不修改现有对象结构的情况下,动态地添加功能或修改行为时,可以使用装饰者模式。
- class Component {
- operation() {
- console.log('Component operation');
- }
- }
-
- class Decorator {
- constructor(component) {
- this.component = component;
- }
- operation() {
- this.component.operation();
- }
- }
-
- class ConcreteComponent extends Component {
- operation() {
- console.log('ConcreteComponent operation');
- }
- }
-
- class ConcreteDecoratorA extends Decorator {
- operation() {
- super.operation();
- console.log('ConcreteDecoratorA operation');
- }
- }
-
- class ConcreteDecoratorB extends Decorator {
- operation() {
- super.operation();
- console.log('ConcreteDecoratorB operation');
- }
- }
-
- const component = new ConcreteComponent();
- const decoratorA = new ConcreteDecoratorA(component);
- const decoratorB = new ConcreteDecoratorB(decoratorA);
-
- decoratorB.operation();
- // Output:
- // Component operation
- // ConcreteComponent operation
- // ConcreteDecoratorA operation
- // ConcreteDecoratorB operation
-
适配器模式(Adapter Pattern):
•优点:允许不兼容接口的对象协同工作,提高代码的复用性和灵活性。
•缺点:增加了代码的复杂性,需要理解和管理适配器的转换过程。
•适用场景:当需要将一个类的接口转换成客户端所期望的另一个接口时,可以使用适配器模式。
- class Target {
- request() {
- console.log('Target request');
- }
- }
-
- class Adaptee {
- specificRequest() {
- console.log('Adaptee specificRequest');
- }
- }
-
- class Adapter extends Target {
- constructor(adaptee) {
- super();
- this.adaptee = adaptee;
- }
- request() {
- this.adaptee.specificRequest();
- }
- }
-
- const target = new Target();
- target.request();
- // Output: Target request
-
- const adaptee = new Adaptee();
- const adapter = new Adapter(adaptee);
- adapter.request();
- // Output: Adaptee specificRequest
-
在上述示例中,Target
定义了客户端所期望的接口,Adaptee
是一个已有的类,它的接口与 Target
不兼容。适配器 Adapter
继承自 Target
,并在其内部持有一个 Adaptee
的引用,通过适配器的 request
方法调用 Adaptee
的 specificRequest
方法,从而实现了对不兼容接口的适配。客户端可以通过调用适配器的 request
方法来使用 Adaptee
的功能。
适配器模式可以用于许多场景,例如在使用第三方库时需要将其接口转换成符合自己代码规范的接口,或者在对旧系统进行重构时需要兼容旧代码和新代码之间的差异。
同源策略指的是:协议,域名,端口相同,同源策略是一种安全协议举例说明:比如一个黑客程序,他利用Iframe把真正的银行登录页面嵌到他的页面上,当你使用真实的用户名,密码登录时,他的页面就可以通过Javascript读取到你的表单中input中的内容,这样用户名,密码就轻松到手了。
同源限制是为了保护用户的隐私和安全而存在的。它的主要目的是防止恶意网站利用客户端脚本对其他网站的信息进行读取和操作,从而避免信息泄露和恶意攻击。
同源策略通过限制来自不同源的网页之间的交互,确保只有同源的网页可以相互访问彼此的资源。同源策略要求协议、域名和端口必须完全相同才能实现同源。如果不满足同源条件,浏览器会禁止跨域请求和操作。
同源限制的作用包括但不限于:
1.防止跨站点脚本攻击(XSS):同源限制可以防止恶意网站通过跨域脚本注入攻击来获取用户敏感信息或操作用户的账户。
2.防止跨站请求伪造(CSRF):同源限制可以防止恶意网站伪造用户请求,以用户的身份执行非法操作。
3.保护用户隐私:同源限制可以防止其他网站通过跨域方式获取用户在当前网站的敏感信息。
同源限制通过浏览器的安全策略实现,确保在不同源的网页之间存在一定的隔离性,提高用户的安全性和隐私保护。但同时也给一些特定的跨域场景带来了限制,因此在需要跨域访问的情况下,可以使用跨域技术(如跨域资源共享CORS、JSONP等)来解决问题。
示例代码中提到的黑客程序利用了跨域嵌套iframe的方式,通过读取用户输入的信息来进行攻击。同源限制可以防止这种攻击,因为该黑客程序的域名与银行登录页面的域名不同,无法通过跨域访问获取用户输入的敏感信息。
常见的兼容性问题有很多,以下列举一些常见的问题:
1.浏览器的盒模型差异:不同浏览器对盒模型的解析存在差异,导致元素的尺寸计算不一致。可以使用CSS盒模型属性(box-sizing
)来进行控制。
2.浏览器对CSS属性的支持差异:不同浏览器对CSS属性的支持程度不同,某些属性在某些浏览器中可能不起作用或解析不正确。需要使用CSS前缀(Vendor Prefix)或使用兼容性方案来处理。
3.JavaScript API的差异:不同浏览器对JavaScript API的支持存在差异,某些方法、属性或事件在某些浏览器中可能不可用或行为不同。需要进行兼容性检测并使用替代方案或进行特定的处理。
4.样式的兼容性:不同浏览器对样式的解析存在差异,可能导致页面显示不一致。需要针对不同浏览器进行样式的调整和优化。
5.图片格式的兼容性:不同浏览器对图片格式的支持存在差异,某些格式在某些浏览器中可能不被支持或显示异常。需要根据需求选择合适的图片格式,并进行兼容性处理。
6.事件处理的差异:不同浏览器对事件的处理存在差异,例如事件对象的属性、方法、坐标获取等方面。需要进行兼容性处理,使用合适的方法来获取事件相关信息。
- // 获取鼠标坐标
- function getMousePosition(event) {
- var x, y;
- if (event.pageX || event.pageY) {
- x = event.pageX;
- y = event.pageY;
- } else {
- x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
- y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
- }
- return { x: x, y: y };
- }
-
- // 兼容性处理
- var event = event || window.event;
- var mousePosition = getMousePosition(event);
-
- // 获取元素样式
- function getComputedStyle(element) {
- if (window.getComputedStyle) {
- return window.getComputedStyle(element, null);
- } else {
- return element.currentStyle;
- }
- }
-
- // 兼容性处理
- var elementStyle = getComputedStyle(element);
-
- // 图片格式兼容性处理
- var img = new Image();
- img.src = 'image.png';
- img.onerror = function() {
- // 图片加载失败,处理兼容性
- };
以上示例代码展示了对常见兼容性问题的处理方法,包括事件对象的属性获取、样式获取和图片加载的兼容性处理。在实际开发中,需要根据具体的兼容性问题选择合适的解决方案,并进行兼容性测试和调整。
ES6(ECMAScript 2015)是JavaScript的第六个主要版本,引入了许多新的语言特性和改进,以提升开发人员的效率和代码质量。以下是ES6的一些重要特性:
•块级作用域:引入let
和const
关键字,允许在块级作用域中声明变量,解决了变量提升和作用域污染的问题。
•箭头函数:使用箭头(=>
)定义函数,简化了函数的书写,并且自动绑定了this
。
•模板字符串:使用反引号(`)包裹字符串,可以在字符串中使用变量和表达式,实现更灵活的字符串拼接和格式化。
•解构赋值:通过解构赋值语法,可以从数组或对象中提取值,并赋给对应的变量,简化了变量赋值的操作。
•默认参数:函数可以定义默认参数值,简化了函数调用时传参的操作。
•扩展运算符:使用三个点(...
)进行数组和对象的展开操作,可以将一个数组或对象拆分为独立的元素,或者将多个数组或对象合并为一个。
•Promise:引入了Promise
对象,用于更好地处理异步操作,解决了回调地狱的问题,并提供了更清晰的异步编程模式。
•类和模块化:ES6引入了类的概念,可以使用class
关键字定义类,实现了更接近传统面向对象编程的方式。同时,ES6还提供了模块化的支持,可以使用import
和export
语法导入和导出模块。
•模块化:引入了模块化的概念,可以使用import
和export
语法导入和导出模块,提供了更好的代码组织和模块复用的方式。
•迭代器和生成器**:引入了迭代器和生成器的概念,可以通过自定义迭代器来遍历数据集合,并使用生成器函数来生成迭代器。
•管道操作符:提案阶段的特性,引入了管道操作符(|>
),可以将表达式的结果作为参数传递给下一个表达式,简化了函数调用和方法链的写法。
•这些特性只是ES6的一部分,还有其他许多特性,如Promise.all
、Map
、Set
、Proxy
、Reflect
等。ES6的引入使得JavaScript语言更加现代化和强大,提供了更多的编程工具和语法
箭头函数
- // 传统函数
- function sum(a, b) {
- return a + b;
- }
-
- // 箭头函数
- const sum = (a, b) => a + b;
-
- // 使用箭头函数作为回调函数
- const numbers = [1, 2, 3, 4, 5];
- const squaredNumbers = numbers.map(num => num * num);
- console.log(squaredNumbers); // [1, 4, 9, 16, 25]
-
模板字符串
- const name = "Alice";
- const age = 25;
-
- // 使用模板字符串进行字符串拼接和变量插值
- const message = `My name is ${name} and I am ${age} years old.`;
- console.log(message); // My name is Alice and I am 25 years old.
-
解构赋值:
- // 数组解构赋值
- const numbers = [1, 2, 3, 4, 5];
- const [first, second, ...rest] = numbers;
- console.log(first); // 1
- console.log(second); // 2
- console.log(rest); // [3, 4, 5]
-
- // 对象解构赋值
- const person = { name: "Alice", age: 25 };
- const { name, age } = person;
- console.log(name); // Alice
- console.log(age); // 25
-
默认参数
- // 函数默认参数
- function greet(name = "Anonymous") {
- console.log(`Hello, ${name}!`);
- }
-
- greet(); // Hello, Anonymous!
- greet("Alice"); // Hello, Alice!
-
扩展运算符
- // 数组展开
- const numbers = [1, 2, 3];
- const combined = [...numbers, 4, 5];
- console.log(combined); // [1, 2, 3, 4, 5]
-
- // 对象展开
- const person = { name: "Alice", age: 25 };
- const copiedPerson = { ...person };
- console.log(copiedPerson); // { name: "Alice", age: 25 }
-
Promise
- // Promise示例
- const fetchData = () => {
- return new Promise((resolve, reject) => {
- setTimeout(() => {
- resolve("Data fetched successfully");
- }, 2000);
- });
- };
-
- fetchData()
- .then(data => {
- console.log(data); // Data fetched successfully
- })
- .catch(error => {
- console.error(error);
- });
-
面向过程编程(Procedural Programming)和面向对象编程(Object-Oriented Programming)是两种不同的编程范式。
面向过程编程是一种以过程(函数、方法)为中心的编程方式,将程序看作是一系列的步骤,通过顺序执行这些步骤来解决问题。面向过程编程强调的是解决问题的步骤和算法,将问题划分为不同的子任务,通过函数的调用和数据的传递来实现任务之间的协作。
面向过程编程的特点:
•程序以过程为单位进行组织,以函数为基本单位。
•数据和函数是分离的,函数对于数据的操作是通过参数进行传递。
•强调算法和步骤,对问题进行分解和抽象,通过顺序、选择、循环等基本结构来控制程序的流程。
•程序的可重用性较低,容易产生大量的重复代码。
面向对象编程是一种以对象为中心的编程方式,将程序看作是一系列相互关联的对象,通过对象之间的交互和消息传递来解决问题。面向对象编程强调的是事物(对象)的抽象、封装、继承和多态性。
面向对象编程的特点:
•程序以对象为单位进行组织,对象是数据和方法的封装体。
•数据和函数(方法)是紧密关联的,对象通过方法来操作自己的数据。
•强调对象之间的关系和交互,通过继承、封装和多态性来实现代码的复用和灵活性。
•程序的可重用性较高,易于维护和扩展。
异同和优缺点:
•异同:
•优点:
•缺点:
面向对象编程(Object-Oriented Programming,简称OOP)是一种编程思想和方法论,其基本思想是以对象为中心,通过封装、继承和多态等机制来组织和管理代码。以下是面向对象编程的基本思想:
1.封装(Encapsulation):将数据和对数据的操作封装在一起,形成对象。对象对外暴露有限的接口,隐藏内部实现细节,提供更好的模块化和抽象性。
2.继承(Inheritance):通过继承机制,一个类(子类)可以继承另一个类(父类)的属性和方法,并可以在此基础上添加、修改或扩展功能。继承可以实现代码的重用性和层次化的组织。
3.多态(Polymorphism):多态是指同一个方法名可以在不同的对象上产生不同的行为。通过多态,可以以统一的方式处理不同类型的对象,提高代码的灵活性和可扩展性。
面向对象编程的优点包括:
•易维护性:对象的封装性和模块化使得代码易于维护和理解。对修改封闭、对扩展开放的原则可以减少对已有代码的影响。
•代码重用性:通过继承和组合等机制,可以重用已有的类和代码,减少重复编写代码的工作量。
•灵活性和可扩展性:面向对象编程提供了灵活的结构和抽象层次,使得代码易于扩展和修改,适应需求的变化。
•可靠性:封装和继承等机制可以提高代码的可靠性和可测试性,减少错误的发生和影响范围。
•可理解性:面向对象的代码通常具有良好的可读性和可理解性,对象和类的设计使得代码更加直观和自然。
面向对象编程可以提供更高层次的抽象和组织,适用于复杂的系统和大型项目的开发。但同时,面向对象编程也有一些限制和挑战,如学习曲线较陡、设计复杂度高、性能开销等。适合选择何种编程思想取决于具体的项目需求和开发场景。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。