diff --git src/wp-includes/class.wp-scripts.php src/wp-includes/class.wp-scripts.php
index 144bfce..a3d0ad5 100644
--- src/wp-includes/class.wp-scripts.php
+++ src/wp-includes/class.wp-scripts.php
@@ -163,6 +163,38 @@ class WP_Scripts extends WP_Dependencies {
 	}
 
 	/**
+	 * Get the expanded source URL for a script.
+	 *
+	 * @since 4.6.0
+	 *
+	 * @param  string $handle The script's handle.
+	 * @return string The URL.
+	 */
+	public function get_script_src( $handle ) {
+		$obj = $this->registered[ $handle ];
+		$src = $obj->src;
+		if ( null === $obj->ver ) {
+			$ver = '';
+		} else {
+			$ver = $obj->ver ? $obj->ver : $this->default_version;
+		}
+
+		// Add any extra arguments to the version string.
+		if ( isset($this->args[ $handle ]) )
+			$ver = $ver ? $ver . '&amp;' . $this->args[ $handle ] : $this->args[ $handle ];
+
+		if ( ! preg_match( '|^(https?:)?//|', $src ) && ! ( $this->content_url && 0 === strpos( $src, $this->content_url ) ) ) {
+			$src = $this->base_url . $src;
+		}
+
+		if ( ! empty( $ver ) )
+			$src = add_query_arg( 'ver', $ver, $src );
+
+		/** This filter is documented in wp-includes/class.wp-scripts.php */
+		return apply_filters( 'script_loader_src', $src, $handle );
+	}
+
+		/**
 	 * Prints scripts.
 	 *
 	 * Prints the scripts passed to it or the print queue. Also prints all necessary dependencies.
@@ -329,20 +361,13 @@ class WP_Scripts extends WP_Dependencies {
 			return true;
 		}
 
-		if ( ! preg_match( '|^(https?:)?//|', $src ) && ! ( $this->content_url && 0 === strpos( $src, $this->content_url ) ) ) {
-			$src = $this->base_url . $src;
-		}
-
-		if ( ! empty( $ver ) )
-			$src = add_query_arg( 'ver', $ver, $src );
-
-		/** This filter is documented in wp-includes/class.wp-scripts.php */
-		$src = esc_url( apply_filters( 'script_loader_src', $src, $handle ) );
+		$src = $this->get_script_src( $handle );
 
-		if ( ! $src )
+		if ( ! $src ) {
 			return true;
-
-		$tag = "{$cond_before}{$before_handle}<script type='text/javascript' src='$src'></script>\n{$after_handle}{$cond_after}";
+		}
+		$attributes = $this->get_script_attributes_html( $handle );
+		$tag = "{$cond_before}{$before_handle}<script$attributes></script>\n{$after_handle}{$cond_after}";
 
 		/**
 		 * Filter the HTML script tag of an enqueued script.
@@ -421,6 +446,64 @@ class WP_Scripts extends WP_Dependencies {
 	}
 
 	/**
+	 * Get the HTML element attributes for a script.
+	 *
+	 * Does not include the `src` attribute.
+	 *
+	 * @since 4.6.0
+	 *
+	 * @param string $handle Script registered handle.
+	 * @return array Attribues.
+	 */
+	public function get_script_attributes( $handle ) {
+		$default_attributes = array(
+			'type' => 'text/javascript',
+			'src' => $this->get_script_src( $handle ),
+		);
+
+		if ( isset( $this->registered[ $handle ]->args['attributes'] ) ) {
+			$extra_attributes = $this->registered[ $handle ]->args['attributes'];
+			$attributes = wp_parse_args( $extra_attributes, $default_attributes );
+		} else {
+			$attributes = $default_attributes;
+		}
+
+		/**
+		 * Filter the HTML element attributes for a script.
+		 *
+		 * @since 4.6.0
+		 *
+		 * @param array  $attributes Array of script element attributes.
+		 * @param string $handle     Script handle.
+		 */
+		$attributes = apply_filters( 'script_additional_attributes', $attributes, $handle );
+		return $attributes;
+	}
+
+
+	/**
+	 * Get the concatenated HTML element attributes for a script.
+	 *
+	 * @since 4.6.0
+	 *
+	 * @param  string $handle The script handle.
+	 * @return string Concatenated attributes.
+	 */
+	public function get_script_attributes_html( $handle ) {
+		$attributes = $this->get_script_attributes( $handle );
+		$html = '';
+		foreach ( $attributes as $attribute => $attribute_value ) {
+			if ( 'src' === $attribute ) {
+				$escaped_attribute_value = esc_url_raw( $attribute_value );
+			} else {
+				$escaped_attribute_value = esc_attr( $attribute_value );
+			}
+			$html .=  sprintf( " %s='%s'", esc_attr_name( $attribute ), $escaped_attribute_value );
+		}
+		return $html;
+	}
+
+	/**
 	 * Localizes a script, only if the script has already been added.
 	 *
 	 * @since 2.1.0
diff --git src/wp-includes/class.wp-styles.php src/wp-includes/class.wp-styles.php
index a175058..b0d9a68 100644
--- src/wp-includes/class.wp-styles.php
+++ src/wp-includes/class.wp-styles.php
@@ -128,6 +128,34 @@ class WP_Styles extends WP_Dependencies {
 	}
 
 	/**
+	 * Get the HTML element attributes for a stylesheet.
+	 *
+	 * Does not include the `href` attribute.
+	 *
+	 * @since 4.6.0
+	 *
+	 * @param  string $handle The stylesheet handle.
+	 * @return array Attribues.
+	 */
+	public function get_style_additional_attributes( $handle ) {
+		$obj = $this->registered[ $handle ];
+		if ( null === $obj->ver ) {
+			$ver = '';
+		} else {
+			$ver = $obj->ver ? $obj->ver : $this->default_version;
+		}
+
+		$attributes = array(
+			'rel' => isset($obj->extra['alt']) && $obj->extra['alt'] ? 'alternate stylesheet' : 'stylesheet',
+			'id' => $handle . '-css',
+			'title' => isset($obj->extra['title']) ? $obj->extra['title'] : '',
+			'type' => 'text/css',
+			'media' => isset( $obj->args ) ? $obj->args : 'all'
+		);
+		return $attributes;
+	}
+
+	/**
 	 * Processes a style dependency.
 	 *
 	 * @since 2.6.0
@@ -200,7 +228,7 @@ class WP_Styles extends WP_Dependencies {
 		 * @param string $href   The stylesheet's source URL.
 		 * @param string $media  The stylesheet's media attribute.
 		 */
-		$tag = apply_filters( 'style_loader_tag', "<link rel='$rel' id='$handle-css' $title href='$href' type='text/css' media='$media' />\n", $handle, $href, $media);
+		$tag = apply_filters( 'style_loader_tag', "<link href='$href'" . $this->get_style_attribute_html( $handle ) . " />\n", $handle, $href, $media);
 		if ( 'rtl' === $this->text_direction && isset($obj->extra['rtl']) && $obj->extra['rtl'] ) {
 			if ( is_bool( $obj->extra['rtl'] ) || 'replace' === $obj->extra['rtl'] ) {
 				$suffix = isset( $obj->extra['suffix'] ) ? $obj->extra['suffix'] : '';
@@ -243,6 +271,23 @@ class WP_Styles extends WP_Dependencies {
 	}
 
 	/**
+	 * Get the concatenated HTML element attributes for a stylesheet
+	 *
+	 * @since 4.6.0
+	 *
+	 * @param  string $handle The stylesheet handle.
+	 * @return string Concatenated attributes.
+	 */
+	public function get_style_attribute_html( $handle ) {
+		$attributes = $this->get_style_additional_attributes( $handle );
+		$html = '';
+		foreach ( $attributes as $attribute => $attribute_value ) {
+			$html .=  sprintf( " %s='%s'", esc_attr_name( $attribute ), esc_attr( $attribute_value ) );
+		}
+		return $html;
+	}
+
+	/**
 	 * Adds extra CSS styles to a registered stylesheet.
 	 *
 	 * @since 3.3.0
diff --git src/wp-includes/functions.wp-scripts.php src/wp-includes/functions.wp-scripts.php
index 0e06218..e9a27ca 100644
--- src/wp-includes/functions.wp-scripts.php
+++ src/wp-includes/functions.wp-scripts.php
@@ -129,6 +129,7 @@ function wp_add_inline_script( $handle, $data, $position = 'after' ) {
  *
  * @since 2.1.0
  * @since 4.3.0 A return value was added.
+ * @since 4.6.0 Introduced the `$attributes` parameter.
  *
  * @param string           $handle    Name of the script. Should be unique.
  * @param string           $src       Full URL of the script, or path of the script relative to the WordPress root directory.
@@ -139,13 +140,21 @@ function wp_add_inline_script( $handle, $data, $position = 'after' ) {
  *                                    If set to null, no version is added.
  * @param bool             $in_footer Optional. Whether to enqueue the script before </body> instead of in the <head>.
  *                                    Default 'false'.
+ * @param array            $args {
+ *     Optional script arguments.
+ *
+ *     @type array $attributes Array of script element attributes.
+ *                             Default: array( 'type' => 'text/javascript' )
+ * }
  * @return bool Whether the script has been registered. True on success, false on failure.
  */
-function wp_register_script( $handle, $src, $deps = array(), $ver = false, $in_footer = false ) {
+function wp_register_script( $handle, $src, $deps = array(), $ver = false, $in_footer = false, $attributes = array() ) {
 	$wp_scripts = wp_scripts();
 	_wp_scripts_maybe_doing_it_wrong( __FUNCTION__ );
 
-	$registered = $wp_scripts->add( $handle, $src, $deps, $ver );
+	$attributes = wp_parse_args( $attributes, array( 'type' => 'text/javascript' ) );
+
+	$registered = $wp_scripts->add( $handle, $src, $deps, $ver, array( 'attributes' => $attributes ) );
 	if ( $in_footer ) {
 		$wp_scripts->add_data( $handle, 'group', 1 );
 	}
@@ -244,6 +253,7 @@ function wp_deregister_script( $handle ) {
  * @see WP_Dependencies::enqueue()
  *
  * @since 2.1.0
+ * @since 4.6.0 Introduced the `$attributes` parameter.
  *
  * @param string           $handle    Name of the script. Should be unique.
  * @param string           $src       Full URL of the script, or path of the script relative to the WordPress root directory.
@@ -254,18 +264,25 @@ function wp_deregister_script( $handle ) {
  *                                    If set to null, no version is added.
  * @param bool             $in_footer Optional. Whether to enqueue the script before </body> instead of in the <head>.
  *                                    Default 'false'.
+ * @param array            $args {
+ *     Optional script arguments.
+ *
+ *     @type array $attributes Array of script element attributes.
+ *                             Default: array( 'type' => 'text/javascript' )
+ * }
  */
-function wp_enqueue_script( $handle, $src = false, $deps = array(), $ver = false, $in_footer = false ) {
+function wp_enqueue_script( $handle, $src = false, $deps = array(), $ver = false, $in_footer = false, $attributes = array() ) {
 	$wp_scripts = wp_scripts();
 
 	_wp_scripts_maybe_doing_it_wrong( __FUNCTION__ );
 
+	$attributes = wp_parse_args( $attributes, array( 'type' => 'text/javascript' ) );
 
 	if ( $src || $in_footer ) {
 		$_handle = explode( '?', $handle );
 
 		if ( $src ) {
-			$wp_scripts->add( $_handle[0], $src, $deps, $ver );
+			$wp_scripts->add( $_handle[0], $src, $deps, $ver, array( 'attributes' => $attributes ) );
 		}
 
 		if ( $in_footer ) {
diff --git tests/phpunit/tests/dependencies/getScriptAttributes.php tests/phpunit/tests/dependencies/getScriptAttributes.php
new file mode 100644
index 0000000..24f84b7
--- /dev/null
+++ tests/phpunit/tests/dependencies/getScriptAttributes.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * @group dependencies
+ * @group scripts
+ */
+class Tests_Dependencies_GetScriptAttributes extends WP_UnitTestCase {
+	var $old_wp_scripts;
+
+	function setUp() {
+		parent::setUp();
+		$this->old_wp_scripts = isset( $GLOBALS['wp_scripts'] ) ? $GLOBALS['wp_scripts'] : null;
+		remove_action( 'wp_default_scripts', 'wp_default_scripts' );
+		$GLOBALS['wp_scripts'] = new WP_Scripts();
+		$GLOBALS['wp_scripts']->default_version = get_bloginfo( 'version' );
+	}
+
+	function tearDown() {
+		$GLOBALS['wp_scripts'] = $this->old_wp_scripts;
+		add_action( 'wp_default_scripts', 'wp_default_scripts' );
+		parent::tearDown();
+	}
+
+	/**
+	 * @ticket 22249
+	 */
+	function test_get_script_attributes_with_defaults() {
+		global $wp_scripts;
+		wp_register_script( 'cool-script', '/cool-script.js', array(), '1' );
+
+		$expected_attributes = array(
+			'type' => 'text/javascript',
+			'src' => '/cool-script.js?ver=1',
+		);
+		$actual_attributes = $wp_scripts->get_script_attributes( 'cool-script' );
+		$this->assertEquals( $expected_attributes, $actual_attributes );
+	}
+
+	/**
+	 * @ticket 22249
+	 */
+	function test_get_script_attributes_with_arbitrary_attributes_added() {
+		global $wp_scripts;
+		wp_register_script( 'cool-script', '/cool-script.js', array(), '1', false, array( 'async' => 'async' ) );
+
+		$expected_attributes = array(
+			'type' => 'text/javascript',
+			'src' => '/cool-script.js?ver=1',
+			'async' => 'async',
+		);
+		$actual_attributes = $wp_scripts->get_script_attributes( 'cool-script' );
+		$this->assertEquals( $expected_attributes, $actual_attributes );
+	}
+
+}
diff --git tests/phpunit/tests/dependencies/getScriptAttributesHtml.php tests/phpunit/tests/dependencies/getScriptAttributesHtml.php
new file mode 100644
index 0000000..5a2a766
--- /dev/null
+++ tests/phpunit/tests/dependencies/getScriptAttributesHtml.php
@@ -0,0 +1,47 @@
+<?php
+/**
+ * @group dependencies
+ * @group scripts
+ */
+class Tests_Dependencies_GetScriptAttributesHtml extends WP_UnitTestCase {
+	var $old_wp_scripts;
+
+	function setUp() {
+		parent::setUp();
+		$this->old_wp_scripts = isset( $GLOBALS['wp_scripts'] ) ? $GLOBALS['wp_scripts'] : null;
+		remove_action( 'wp_default_scripts', 'wp_default_scripts' );
+		$GLOBALS['wp_scripts'] = new WP_Scripts();
+		$GLOBALS['wp_scripts']->default_version = get_bloginfo( 'version' );
+	}
+
+	function tearDown() {
+		$GLOBALS['wp_scripts'] = $this->old_wp_scripts;
+		add_action( 'wp_default_scripts', 'wp_default_scripts' );
+		parent::tearDown();
+	}
+
+	/**
+	 * @ticket 22249
+	 */
+	function test_get_script_attributes_html_with_defaults() {
+		global $wp_scripts;
+		wp_register_script( 'cool-script', '/cool-script.js', array(), '1' );
+
+		$actual = $wp_scripts->get_script_attributes_html( 'cool-script' );
+		$expected = " type='text/javascript' src='/cool-script.js?ver=1'";
+		$this->assertEquals( $expected, $actual );
+	}
+
+	/**
+	 * @ticket 22249
+	 */
+	function test_get_script_attributes_html_with_arbitrary_attributes_added() {
+		global $wp_scripts;
+		wp_register_script( 'cool-script', '/cool-script.js', array(), '1', false, array( 'async' => 'async' ) );
+
+		$actual = $wp_scripts->get_script_attributes_html( 'cool-script' );
+		$expected = " type='text/javascript' src='/cool-script.js?ver=1' async='async'";
+		$this->assertEquals( $expected, $actual );
+	}
+
+}
diff --git tests/phpunit/tests/dependencies/getScriptSrc.php tests/phpunit/tests/dependencies/getScriptSrc.php
new file mode 100644
index 0000000..64d7007
--- /dev/null
+++ tests/phpunit/tests/dependencies/getScriptSrc.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ * @group dependencies
+ * @group scripts
+ */
+class Tests_Dependencies_GetScriptSrc extends WP_UnitTestCase {
+	var $old_wp_scripts;
+
+	function setUp() {
+		parent::setUp();
+		$this->old_wp_scripts = isset( $GLOBALS['wp_scripts'] ) ? $GLOBALS['wp_scripts'] : null;
+		remove_action( 'wp_default_scripts', 'wp_default_scripts' );
+		$GLOBALS['wp_scripts'] = new WP_Scripts();
+		$GLOBALS['wp_scripts']->default_version = get_bloginfo( 'version' );
+	}
+
+	function tearDown() {
+		$GLOBALS['wp_scripts'] = $this->old_wp_scripts;
+		add_action( 'wp_default_scripts', 'wp_default_scripts' );
+		parent::tearDown();
+	}
+
+	/**
+	 * @ticket 22249
+	 */
+	function test_get_script_src_for_script_with_relative_url() {
+		global $wp_scripts;
+		wp_register_script( 'cool-script', '/cool-script.js', array(), '1' );
+
+		$actual = $wp_scripts->get_script_src( 'cool-script' );
+		$this->assertEquals( '/cool-script.js?ver=1', $actual );
+	}
+
+	/**
+	 * @ticket 22249
+	 */
+	function test_get_script_src_for_script_with_full_url() {
+		global $wp_scripts;
+		wp_register_script( 'd3', 'https://d3js.org/d3.v3.min.js', array(), '1' );
+
+		$actual = $wp_scripts->get_script_src( 'd3' );
+		$this->assertEquals( 'https://d3js.org/d3.v3.min.js?ver=1', $actual );
+	}
+
+}
diff --git tests/phpunit/tests/dependencies/scripts.php tests/phpunit/tests/dependencies/scripts.php
index efe5a7b..01c56ab 100644
--- tests/phpunit/tests/dependencies/scripts.php
+++ tests/phpunit/tests/dependencies/scripts.php
@@ -724,4 +724,31 @@ class Tests_Dependencies_Scripts extends WP_UnitTestCase {
 
 		$this->assertEquals( $expected, get_echo( 'wp_print_scripts' ) );
 	}
+
+	/**
+	 * @ticket 22249
+	 */
+	function test_wp_register_script_should_allow_arbitrary_element_attributes() {
+		wp_register_script( 'cool-script', '/cool-script.js', array(), '1', false, array( 'async' => 'async' ) );
+
+		wp_enqueue_script( 'cool-script' );
+
+		$header = get_echo( 'wp_print_head_scripts' );
+		$expected_header  = "<script type='text/javascript' src='/cool-script.js?ver=1' async='async'></script>\n";
+
+		$this->assertEquals( $expected_header, $header );
+	}
+
+	/**
+	 * @ticket 22249
+	 */
+	function test_wp_enqueue_script_should_allow_arbitrary_element_attributes() {
+		wp_enqueue_script( 'cool-script', '/cool-script.js', array(), '1', false, array( 'async' => 'async' ) );
+
+		$header = get_echo( 'wp_print_head_scripts' );
+		$expected_header  = "<script type='text/javascript' src='/cool-script.js?ver=1' async='async'></script>\n";
+
+		$this->assertEquals( $expected_header, $header );
+	}
+
 }
